Compare commits

..
182 changed files with 1108 additions and 4507 deletions
+1 -46
View File
@@ -92,7 +92,6 @@ jobs:
outputs:
core-ci: ${{ steps.determine.outputs.core-ci }}
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-run-all: ${{ steps.determine.outputs.integration-run-all }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
@@ -153,9 +152,6 @@ jobs:
# Extract individual fields
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
# A missing key must fail here, not silently disable the junit upload
run_all=$(echo "$output" | jq -r 'if has("integration_run_all") then .integration_run_all else error("integration_run_all missing") end')
echo "integration-run-all=${run_all}" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
@@ -359,15 +355,6 @@ jobs:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# What the cache steps persist; libdeps is excluded (keyed per xdist
# worker and env, it never crosses runs).
INTEGRATION_PIO_CACHE_PATH: |
~/.esphome-integration-tests/platformio/platforms
~/.esphome-integration-tests/platformio/packages
~/.esphome-integration-tests/platformio/appstate.json
~/.esphome-integration-tests/platformio/.cache
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -386,14 +373,6 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration PlatformIO cache
# Native platform + toolchain installed by shared_platformio_cache in
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
id: pio-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -431,36 +410,12 @@ jobs:
run: |
. venv/bin/activate
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
if [ "${#test_files[@]}" -eq 0 ]; then
echo "::error::Empty integration test bucket; pytest would collect the whole tree"
exit 1
fi
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
--junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload junit timings
# Consumed by sync-integration-durations.yml through
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
if-no-files-found: error
# A full cron period of margin for the weekly refresh
retention-days: 14
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
- name: Print ccache statistics
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save integration PlatformIO cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
@@ -1,98 +0,0 @@
---
name: Refresh integration test durations
on:
workflow_dispatch:
schedule:
- cron: "45 5 * * 1"
# Repo writes (branch push, PR open) happen via the App token minted below,
# so the workflow's GITHUB_TOKEN does not need any write scopes.
permissions:
contents: read
actions: read # gh api / gh run download for the CI junit artifacts
jobs:
sync:
name: Refresh integration test durations
runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
permission-contents: write # push the sync branch
permission-pull-requests: write # open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Refresh from the newest usable dev run
env:
GH_TOKEN: ${{ github.token }}
run: |
# Only full matrix dev runs upload junit-integration-* artifacts
# (see the integration-tests job); the merge script re-checks
# coverage regardless.
# Newest-first candidates via their bucket-0 artifact. Fork PRs run
# their own ci.yml, so name and branch are spoofable; require
# same-repo. Assignment failures trip set -e and fail loudly.
candidates=$(
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=junit-integration-0&per_page=100" \
--jq '.artifacts[] | select(.expired | not) | .workflow_run
| select(.head_branch == "dev" and .head_repository_id != null
and .head_repository_id == .repository_id)
| .id'
)
# Green runs first, then the rest newest first; a run missing a
# bucket fails the coverage check and the next one is tried
green=""
rest=""
for id in ${candidates}; do
conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${id}" --jq '.conclusion // ""')
if [ "${conclusion}" = "success" ]; then
green="${green} ${id}"
elif [ -n "${conclusion}" ]; then
rest="${rest} ${id}"
fi
done
# helpers.py imports colorama; the script needs nothing else
pip install colorama
for id in ${green} ${rest}; do
rm -rf /tmp/junit
if ! gh run download "${id}" --repo "${GITHUB_REPOSITORY}" -p "junit-integration-*" -D /tmp/junit; then
echo "::warning::Could not download artifacts for run ${id}; trying the next"
continue
fi
status=0
python script/update_integration_test_durations.py /tmp/junit || status=$?
if [ "${status}" -eq 0 ]; then
echo "Refreshed from run ${id}"
exit 0
fi
# Only EXIT_LOW_COVERAGE (3) from the script advances to the next run
[ "${status}" -eq 3 ] || exit 1
echo "::warning::Run ${id} covers too few test files; trying the next"
done
echo "::error::No dev CI run with usable junit artifacts in range; the feed is starved"
exit 1
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "[ci] Refresh integration test durations"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
branch: sync/integration-durations
delete-branch: true
title: "[ci] Refresh integration test durations"
body-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ steps.generate-token.outputs.token }}
+1 -5
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import argparse
import os
import re
CHANNEL_DEV = "dev"
@@ -65,10 +64,7 @@ def main():
suffix = f"-{args.suffix}" if args.suffix else ""
repository = (
(os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower()
)
image_name = f"{repository}{suffix}"
image_name = f"esphome/esphome{suffix}"
print(f"channel={channel}")
+17 -16
View File
@@ -1670,26 +1670,20 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
if exit_code != 0:
return exit_code
if CORE.is_host:
_LOGGER.info(
"Successfully compiled program to path '%s'", _host_program_path(config)
)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0
def _host_program_path(config: ConfigType) -> str:
"""Return the compiled host ELF path."""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return str(toolchain.get_elf_path())
from esphome.platformio.toolchain import get_idedata
# Memoized by compile_program's own call; this is a dict lookup
return str(get_idedata(config).firmware_elf_path)
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
@@ -1734,7 +1728,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
program_path = _host_program_path(config)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
-1
View File
@@ -78,7 +78,6 @@ from esphome.cpp_types import ( # noqa: F401
StringRef,
arduino_json_ns,
bool_,
char,
const_char_ptr,
double,
esphome_ns,
-23
View File
@@ -13,7 +13,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
get_esp32_variant,
)
import esphome.config_validation as cv
@@ -157,17 +156,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
42: adc_channel_t.ADC_CHANNEL_0,
43: adc_channel_t.ADC_CHANNEL_1,
44: adc_channel_t.ADC_CHANNEL_2,
45: adc_channel_t.ADC_CHANNEL_3,
46: adc_channel_t.ADC_CHANNEL_4,
47: adc_channel_t.ADC_CHANNEL_5,
48: adc_channel_t.ADC_CHANNEL_6,
49: adc_channel_t.ADC_CHANNEL_7,
},
}
# pin to adc2 channel mapping
@@ -237,17 +225,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
50: adc_channel_t.ADC_CHANNEL_0,
51: adc_channel_t.ADC_CHANNEL_1,
52: adc_channel_t.ADC_CHANNEL_2,
53: adc_channel_t.ADC_CHANNEL_3,
54: adc_channel_t.ADC_CHANNEL_4,
55: adc_channel_t.ADC_CHANNEL_5,
56: adc_channel_t.ADC_CHANNEL_6,
57: adc_channel_t.ADC_CHANNEL_7,
},
}
+38 -40
View File
@@ -74,7 +74,9 @@ void ADCSensor::setup() {
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants (except C2) and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -92,7 +94,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
@@ -110,11 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // No calibration scheme available
(void) handle;
ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated");
this->setup_flags_.calibration_complete = false;
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
}
this->setup_flags_.init_complete = true;
@@ -123,28 +121,23 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED";
#else
const char *calibration_status = "N/A"; // This variant has no calibration scheme
#endif
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED",
this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status,
this->setup_flags_.init_complete ? "OK" : "FAILED");
ESP_LOGCONFIG(
TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this);
}
@@ -191,11 +184,12 @@ float ADCSensor::sample_fixed_attenuation_() {
} else {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr;
}
}
@@ -223,9 +217,10 @@ float ADCSensor::sample_autorange_() {
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
@@ -234,7 +229,8 @@ float ADCSensor::sample_autorange_() {
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -246,7 +242,7 @@ float ADCSensor::sample_autorange_() {
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);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
@@ -268,9 +264,10 @@ float ADCSensor::sample_autorange_() {
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
@@ -289,9 +286,10 @@ float ADCSensor::sample_autorange_() {
ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage);
}
// Clean up calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
-9
View File
@@ -3,7 +3,6 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
@@ -57,14 +56,6 @@ def validate_config(config: ConfigType) -> ConfigType:
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
# The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1)
if (
CORE.is_esp32
and get_esp32_variant() == VARIANT_ESP32S31
and config.get(CONF_ATTENUATION, "0db") != "0db"
):
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set"
+13 -219
View File
@@ -5,7 +5,6 @@ from typing import Any
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION, CONF_HOST
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
@@ -24,8 +23,6 @@ from esphome.const import (
CONF_CAPTURE_RESPONSE,
CONF_DATA,
CONF_DATA_TEMPLATE,
CONF_DELAY,
CONF_ENABLE_IPV6,
CONF_ENCRYPTION,
CONF_EVENT,
CONF_ID,
@@ -44,13 +41,10 @@ from esphome.const import (
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
@@ -131,12 +125,10 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
}
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_EXAMPLE = "example"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_OUTGOING_CONNECTION = "outgoing_connection"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
@@ -236,30 +228,14 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_EXAMPLE): cv.string_strict,
}
)
# Accepts the plain `name: type` shorthand or the full mapping form
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: validate_variable,
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
# No default - auto-detected by _auto_detect_supports_response
@@ -288,44 +264,9 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
# (not max_connections, which is the upper limit rarely reached)
socket.consume_sockets(3, "api")(config)
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
if CONF_OUTGOING_CONNECTION in config:
socket.consume_sockets(1, "api_outgoing_connection")(config)
return config
def _validate_outgoing_connection(config: ConfigType) -> ConfigType:
if CONF_OUTGOING_CONNECTION not in config:
return config
if CONF_ENCRYPTION not in config:
raise cv.Invalid(
"outgoing_connection requires 'encryption' so the peer is verified by key",
path=[CONF_OUTGOING_CONNECTION],
)
return config
_OUTGOING_CONNECTION_SCHEMA = cv.Schema(
{
cv.Optional(CONF_HOST): cv.ipaddress,
cv.Optional(CONF_PORT, default=6054): cv.port,
# Bounded to half the device's uint32 millisecond range so the wait
# always elapses under a wrapping clock
cv.Optional(CONF_DELAY, default="60s"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=2147483647)),
),
}
)
def _outgoing_connection_schema(config: ConfigType | None) -> ConfigType:
# A bare `outgoing_connection:` block is valid; without a host the device
# dials the remembered last dial-back client
if config is None:
config = {}
return _OUTGOING_CONNECTION_SCHEMA(config)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -350,7 +291,6 @@ CONFIG_SCHEMA = cv.All(
): ACTIONS_SCHEMA,
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_OUTGOING_CONNECTION): _outgoing_connection_schema,
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
@@ -407,136 +347,11 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_outgoing_connection,
_consume_api_sockets,
_register_provisioning_source,
)
def _has_action_metadata(actions: list[ConfigType]) -> bool:
# Empty strings count as unset, matching _action_strings
return any(
conf.get(CONF_DESCRIPTION)
or any(
var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE)
for var_ in conf[CONF_VARIABLES].values()
)
for conf in actions
)
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
# An empty description or example is treated as unset
strings: list[str | None] = [conf[CONF_ACTION]]
if has_metadata:
strings.append(conf.get(CONF_DESCRIPTION) or None)
for name, var_ in conf[CONF_VARIABLES].items():
strings.append(name)
if has_metadata:
strings += [
var_.get(CONF_DESCRIPTION) or None,
var_.get(CONF_EXAMPLE) or None,
]
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
"""Reject the raw lwip_tcp socket, the only option on ESP8266 and RP2040.
Checked against the resolved implementation so an explicit selection on
another platform is caught the same way as the platform default.
"""
if CONF_OUTGOING_CONNECTION not in config:
return config
from esphome.components import socket
socket_conf = fv.full_config.get().get("socket") or {}
if (
impl := socket_conf.get(socket.CONF_IMPLEMENTATION)
) in socket.IMPLEMENTATIONS_WITHOUT_CONNECT:
raise cv.Invalid(
f"outgoing_connection is not supported with the {impl} socket "
"implementation (the only one on ESP8266 and RP2040) because it "
"cannot make outgoing connections",
path=[CONF_OUTGOING_CONNECTION],
)
return config
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
(outgoing := config.get(CONF_OUTGOING_CONNECTION)) is None
or (host := outgoing.get(CONF_HOST)) is None
or host.version != 6
):
return config
network_conf = fv.full_config.get().get("network") or {}
if not network_conf.get(CONF_ENABLE_IPV6):
raise cv.Invalid(
"outgoing_connection host is an IPv6 address but IPv6 is not "
"enabled; set 'network: enable_ipv6: true'",
path=[CONF_OUTGOING_CONNECTION, CONF_HOST],
)
return config
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_esp8266_action_strings,
_validate_outgoing_socket_implementation,
_validate_outgoing_host_ipv6,
)
def _add_action_strings(
index: int, strings: list[str | None], interned: dict[str, MockObj]
) -> MockObj:
"""Emit the PROGMEM string table for one action.
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
strings are shared between actions through `interned`.
"""
entries: list[MockObj] = []
for string in strings:
if string is None:
entries.append(cg.nullptr)
continue
if (var := interned.get(string)) is None:
var = interned[string] = cg.progmem_array(
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
string,
)
entries.append(var)
return cg.progmem_array(
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
entries,
)
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -556,10 +371,8 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
actions = config.get(CONF_ACTIONS, [])
has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES]
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
if has_user_actions:
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
@@ -572,17 +385,10 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
has_metadata = _has_action_metadata(actions)
if has_metadata:
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
interned_strings: dict[str, MockObj] = {}
if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.MockObj] = []
for index, conf in enumerate(actions):
for conf in actions:
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
@@ -615,23 +421,22 @@ async def to_code(config: ConfigType) -> None:
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
var_type = var_[CONF_TYPE]
if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_type]
if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_]
else:
native = SERVICE_ARG_NATIVE_TYPES[var_type]
native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native)
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings, interned_strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
)
triggers.append(trigger)
auto = await automation.build_automation(trigger, func_args, conf)
@@ -653,9 +458,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
@@ -692,13 +494,6 @@ async def to_code(config: ConfigType) -> None:
else:
cg.add_define("USE_API_PLAINTEXT")
if (outgoing := config.get(CONF_OUTGOING_CONNECTION)) is not None:
cg.add_define("USE_API_OUTGOING_CONNECTION")
if (host := outgoing.get(CONF_HOST)) is not None:
cg.add_define("API_OUTGOING_CONNECTION_HOST", str(host))
cg.add_define("API_OUTGOING_CONNECTION_PORT", outgoing[CONF_PORT])
cg.add_define("API_OUTGOING_CONNECTION_DELAY", outgoing[CONF_DELAY])
cg.add_define("USE_API")
cg.add_global(api_ns.using)
@@ -1085,7 +880,6 @@ _define_filter = filter_source_files_from_defines(
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
"api_frame_helper_noise.cpp": "USE_API_NOISE",
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
"api_outgoing_connection.cpp": "USE_API_OUTGOING_CONNECTION",
}
)
-12
View File
@@ -112,11 +112,6 @@ message HelloRequest {
string client_info = 1;
uint32 api_version_major = 2;
uint32 api_version_minor = 3;
// Set by clients that can accept connections the device opens to them
// (see api: outgoing_connection:). The device remembers this client's
// address as the target to dial when no such client is connected.
bool outgoing_connection_target = 4 [(field_ifdef) = "USE_API_OUTGOING_CONNECTION"];
}
// Confirmation of successful connection request.
@@ -336,10 +331,6 @@ message DeviceInfoResponse {
// all-zeros PSK, so the api encryption key can be provisioned without being
// sent in plaintext (protects against passive sniffing, not active MITM)
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
// Device is built with the api outgoing_connection option and can open
// the TCP connection to a dial-back target itself
bool api_outgoing_connection_supported = 27 [(field_ifdef) = "USE_API_OUTGOING_CONNECTION"];
}
// ==================== DEVICE CAPABILITIES ====================
@@ -1043,8 +1034,6 @@ message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
@@ -1055,7 +1044,6 @@ message ListEntitiesServicesResponse {
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
-16
View File
@@ -1822,19 +1822,6 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
this->complete_authentication_();
#ifdef USE_API_OUTGOING_CONNECTION
// With a PSK set only key-verified transports reach hello: plaintext and
// zero-PSK are rejected, and pre-activation sessions are force-closed
if (msg.outgoing_connection_target && !this->flags_.outgoing_connection_target) {
if (this->parent_->get_noise_ctx().has_psk()) {
this->flags_.outgoing_connection_target = true;
this->parent_->on_outgoing_target_client(this);
} else {
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Dial-back target refused; no key active"));
}
}
#endif
return this->send_message(resp);
}
@@ -1957,9 +1944,6 @@ bool APIConnection::send_device_info_response_() {
// one) so this advertisement survives the plaintext removal in 2027.2.0.
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
#endif
#ifdef USE_API_OUTGOING_CONNECTION
resp.api_outgoing_connection_supported = true;
#endif
#endif
#ifdef USE_DEVICES
size_t device_index = 0;
-18
View File
@@ -375,21 +375,6 @@ class APIConnection final : public APIServerConnectionBase {
return this->helper_->get_peername_to(buf);
}
#ifdef USE_API_OUTGOING_CONNECTION
/// Outgoing connection: send our server hello immediately so the peer can
/// pick the matching key. Outgoing connections are only dialed when a PSK
/// is set, so the helper is always the noise helper. Call after start().
void mark_outgoing() {
if (this->flags_.remove) {
return; // start() failed; the connection is already being torn down
}
APIError err = static_cast<APINoiseFrameHelper *>(this->helper_.get())->send_server_hello_first();
if (err != APIError::OK) {
this->fatal_error_with_log_(LOG_STR("Server hello failed"), err);
}
}
#endif
protected:
bool try_to_clear_buffer_slow_(bool log_out_of_space);
@@ -760,9 +745,6 @@ class APIConnection final : public APIServerConnectionBase {
uint8_t batch_first_message : 1; // For batch buffer allocation
uint8_t should_try_send_immediately : 1; // True after initial states are sent
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
#ifdef USE_API_OUTGOING_CONNECTION
uint8_t outgoing_connection_target : 1; // Client declared itself a dial-back target in its hello
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
uint8_t log_only_mode : 1;
#endif
+1 -2
View File
@@ -282,8 +282,7 @@ class APIFrameHelper {
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
CLIENT_HELLO_OUTGOING = 9, // Noise only: like CLIENT_HELLO but the server hello already went out (outgoing conn)
EXPLICIT_REJECT = 8, // Noise only
};
// Fast inline state check for read_packet/write_protobuf_messages hot path.
@@ -81,13 +81,6 @@ APIError APINoiseFrameHelper::init() {
state_ = State::CLIENT_HELLO;
return APIError::OK;
}
#ifdef USE_API_OUTGOING_CONNECTION
APIError APINoiseFrameHelper::send_server_hello_first() {
// The peer needs our name and MAC to pick the key before its first message
this->state_ = State::CLIENT_HELLO_OUTGOING;
return this->send_server_hello_frame_();
}
#endif
#ifdef USE_API_PLAINTEXT
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
APIError err = this->init();
@@ -260,9 +253,6 @@ APIError APINoiseFrameHelper::state_action_() {
HELPER_LOG("Bad state for method: %d", (int) this->state_);
return APIError::BAD_STATE;
case State::CLIENT_HELLO:
#ifdef USE_API_OUTGOING_CONNECTION
case State::CLIENT_HELLO_OUTGOING:
#endif
return this->state_action_client_hello_();
case State::SERVER_HELLO:
return this->state_action_server_hello_();
@@ -295,16 +285,11 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
#ifdef USE_API_OUTGOING_CONNECTION
if (this->state_ == State::CLIENT_HELLO_OUTGOING) {
// Server hello already went out at handoff
return this->start_handshake_();
}
#endif
state_ = State::SERVER_HELLO;
return APIError::OK;
}
APIError APINoiseFrameHelper::send_server_hello_frame_() {
APIError APINoiseFrameHelper::state_action_server_hello_() {
// send server hello
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
@@ -328,18 +313,15 @@ APIError APINoiseFrameHelper::send_server_hello_frame_() {
// node mac, terminated by null byte
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
return write_frame_(msg, total_size);
}
APIError APINoiseFrameHelper::state_action_server_hello_() {
APIError aerr = this->send_server_hello_frame_();
APIError aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
return this->start_handshake_();
}
APIError APINoiseFrameHelper::start_handshake_() {
APIError aerr = init_handshake_();
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
return APIError::OK;
}
@@ -28,12 +28,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
// Seeds the already-read header bytes and pumps the handshake state machine
// until it would block.
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
#endif
#ifdef USE_API_OUTGOING_CONNECTION
// Send the server hello immediately so the peer can pick the key before
// its PSK-mixed message. Call after init(); the mode is tracked in state_
// so the helper does not grow.
APIError send_server_hello_first();
#endif
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
@@ -45,8 +39,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError state_action_();
APIError state_action_client_hello_();
APIError state_action_server_hello_();
APIError send_server_hello_frame_();
APIError start_handshake_();
APIError state_action_handshake_();
APIError state_action_handshake_read_();
APIError state_action_handshake_write_();
@@ -1,236 +0,0 @@
#include "api_outgoing_connection.h"
#if defined(USE_API) && defined(USE_API_OUTGOING_CONNECTION)
#include "api_connection.h"
#include "api_server.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cerrno>
#include <cinttypes>
#include <cstring>
namespace esphome::api {
static const char *const TAG = "api.outgoing";
void OutgoingConnectionManager::setup() {
#ifndef API_OUTGOING_CONNECTION_HOST
this->target_pref_ = global_preferences->make_preference<SavedOutgoingTarget>(629847102UL, true);
if (this->target_pref_.load(&this->saved_)) {
// Defend against a corrupt or truncated blob before the first read
this->saved_.host[sizeof(this->saved_.host) - 1] = '\0';
this->host_persisted_ = true;
ESP_LOGD(TAG, "Loaded target %s", this->saved_.host);
} else {
// Never saved, or the blob failed its size/CRC check
ESP_LOGD(TAG, "No saved target");
this->saved_ = {};
}
#endif
}
void OutgoingConnectionManager::loop(APIServer *server) {
if (server->has_outgoing_target_client_()) {
return; // on_target_client() already reset the dial state
}
if (this->dialed_conn_ != nullptr) {
// A live dialed session (flagged or not, e.g. a host: peer) is the
// target; a silent one dies on the handshake timeout
return;
}
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case DialState::DIAL_STATE_IDLE:
#ifdef USE_DEEP_SLEEP
// A deep sleep wake window is too short to spend on the delay
this->schedule_wait_(now, BACKOFF_MIN_MS);
#else
// Target went away; give it the configured delay to reconnect first
this->schedule_wait_(now, API_OUTGOING_CONNECTION_DELAY);
#endif
break;
case DialState::DIAL_STATE_WAITING:
if (now - this->state_ts_ >= this->wait_) {
this->try_dial_(server, now);
}
break;
case DialState::DIAL_STATE_CONNECTING:
this->poll_connect_(server, now);
break;
}
}
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
if (!network::is_connected()) {
// Flips within seconds of boot; recheck fast so a deep sleep wake
// window is not spent waiting
this->schedule_wait_(now, NETWORK_RETRY_MS);
return;
}
const char *host = this->target_host_();
if (host == nullptr) {
// The steady state until a dial-back client has ever connected
ESP_LOGV(TAG, "Not dialing: no target");
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
return;
}
const bool at_limit = server->at_client_limit_();
if (at_limit || !server->noise_ctx_.has_psk()) {
ESP_LOGD(TAG, "Not dialing: %s", at_limit ? "max connections" : "no key");
// Not a dial failure; retry without escalating the backoff
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
return;
}
struct sockaddr_storage addr;
socklen_t addr_len =
socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, API_OUTGOING_CONNECTION_PORT);
if (addr_len == 0) {
ESP_LOGW(TAG, "Invalid target %s", host);
#ifndef API_OUTGOING_CONNECTION_HOST
// A corrupt remembered value can never become dialable; forget it
// (covers an IPv6 literal left by an earlier enable_ipv6 build too)
this->saved_ = {};
if (!this->persist_target_()) {
ESP_LOGW(TAG, "Failed to clear target");
}
#endif
this->schedule_retry_(now);
return;
}
this->dial_socket_ = socket::socket_loop_monitored(((struct sockaddr *) &addr)->sa_family, SOCK_STREAM, IPPROTO_TCP);
if (!this->dial_socket_ || this->dial_socket_->setblocking(false) != 0) {
ESP_LOGW(TAG, "Socket %s failed: errno %d", this->dial_socket_ ? "setblocking" : "create", errno);
this->schedule_retry_(now);
return;
}
ESP_LOGD(TAG, "Dialing %s:%u", host, API_OUTGOING_CONNECTION_PORT);
int err = this->dial_socket_->connect((struct sockaddr *) &addr, addr_len);
if (err == 0) {
// Immediate success (possible for localhost)
this->handoff_(server, now);
return;
}
if (errno != EINPROGRESS) {
ESP_LOGW(TAG, "Connect failed: errno %d", errno);
this->schedule_retry_(now);
return;
}
this->state_ = DialState::DIAL_STATE_CONNECTING;
this->state_ts_ = now;
this->last_poll_ = now;
}
void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
if (now - this->state_ts_ >= CONNECT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Connect timeout");
this->schedule_retry_(now);
return;
}
if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
return;
}
this->last_poll_ = now;
int err = 0;
switch (socket::poll_connect(*this->dial_socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
break;
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
this->handoff_(server, now);
break;
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
ESP_LOGW(TAG, "Connect failed: %d", err);
this->schedule_retry_(now);
break;
}
}
void OutgoingConnectionManager::handoff_(APIServer *server, uint32_t now) {
this->dialed_conn_ = server->add_outgoing_client_(std::move(this->dial_socket_));
if (this->dialed_conn_ == nullptr) {
// Only preconditions (slot limit, key cleared) refuse the handoff; the
// peer is reachable, so do not escalate the backoff
this->schedule_wait_(now, PRECONDITION_RETRY_MS);
return;
}
// Connected; dialed_conn_ gates further dialing until the session settles
this->state_ = DialState::DIAL_STATE_IDLE;
}
void OutgoingConnectionManager::schedule_wait_(uint32_t now, uint32_t wait) {
this->dial_socket_.reset(); // no-op when the socket was handed off
this->state_ = DialState::DIAL_STATE_WAITING;
this->state_ts_ = now;
this->wait_ = wait;
}
void OutgoingConnectionManager::schedule_retry_(uint32_t now) {
// +/-20% jitter so a fleet of devices does not retry one server in lockstep
const uint32_t jitter_span = this->backoff_ / 5;
this->schedule_wait_(now, this->backoff_ - jitter_span + (random_uint32() % (2 * jitter_span + 1)));
this->backoff_ = std::min(this->backoff_ * 2, BACKOFF_MAX_MS);
}
void OutgoingConnectionManager::on_client_removed(APIConnection *conn, bool was_authenticated) {
if (conn != this->dialed_conn_) {
return;
}
this->dialed_conn_ = nullptr;
if (was_authenticated) {
// A working peer (e.g. a host: target that never sends the flag)
// disconnected normally; state is IDLE, so loop() applies the delay
this->backoff_ = BACKOFF_MIN_MS;
} else {
this->schedule_retry_(App.get_loop_component_start_time());
}
}
void OutgoingConnectionManager::on_target_client(APIConnection *conn) {
// The target is connected; stop any dial in flight and reset the backoff.
// A dialed connection stays tracked unless it is this one: an inbound
// target must not orphan a still-open dial.
this->dial_socket_.reset();
if (conn == this->dialed_conn_) {
this->dialed_conn_ = nullptr;
}
this->state_ = DialState::DIAL_STATE_IDLE;
this->backoff_ = BACKOFF_MIN_MS;
#ifndef API_OUTGOING_CONNECTION_HOST
SavedOutgoingTarget target{};
conn->get_peername_to(target.host);
if (target.host[0] == '\0') {
ESP_LOGW(TAG, "Could not read peer address; not remembering target");
return;
}
if (this->host_persisted_ && strcmp(target.host, this->saved_.host) == 0) {
return; // unchanged and already on flash; avoid flash wear
}
// Use the fresh address this boot even if the flash write fails; a failed
// write is retried on the next flagged hello via host_persisted_
this->saved_ = target;
if (!this->persist_target_()) {
ESP_LOGW(TAG, "Failed to save target");
return;
}
ESP_LOGD(TAG, "Saved %s as outgoing connection target", this->saved_.host);
#endif
}
void OutgoingConnectionManager::dump_config() const {
const char *host = this->target_host_();
if (host == nullptr) {
host = "none remembered yet";
}
// The boot delay differs from delay: on deep sleep builds, so print the
// value that actually applies
ESP_LOGCONFIG(TAG,
" Outgoing connection port: %u\n"
" Outgoing connection host: %s\n"
" Outgoing connection boot delay: %" PRIu32 "ms",
API_OUTGOING_CONNECTION_PORT, host, BOOT_WAIT_MS);
}
} // namespace esphome::api
#endif // USE_API && USE_API_OUTGOING_CONNECTION
@@ -1,117 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_API) && defined(USE_API_OUTGOING_CONNECTION)
#ifdef USE_SOCKET_IMPL_LWIP_TCP
#error "api outgoing_connection needs a socket implementation that can make outgoing connections"
#endif
#ifndef USE_API_NOISE
#error "api outgoing_connection needs noise encryption so the peer is verified by key"
#endif
#include "esphome/components/socket/socket.h"
#include "esphome/core/preferences.h"
#include <memory>
namespace esphome::api {
class APIServer;
class APIConnection;
// Follows the build's address family (ifdef'd in socket/headers.h): toggling
// enable_ipv6 changes the blob size, load() rejects the old blob, and the
// target is simply relearned
static constexpr size_t SAVED_TARGET_HOST_LEN = socket::SOCKADDR_STR_LEN;
struct SavedOutgoingTarget {
// IP as text so the socket component's v4-mapped-IPv6 normalization is
// reused on both ends; empty = none remembered
char host[SAVED_TARGET_HOST_LEN];
} PACKED; // NOLINT
/// Dials out when no dial-back target client is connected. Only the TCP
/// direction flips: the device stays the Noise responder, so both sides
/// still verify by key. Targets the YAML host or the last remembered client.
class OutgoingConnectionManager {
public:
void setup();
void loop(APIServer *server);
/// A key-verified client declared itself a dial-back target; last one wins
void on_target_client(APIConnection *conn);
/// Clears the dialed-connection gate; dying unauthenticated escalates the backoff
void on_client_removed(APIConnection *conn, bool was_authenticated);
void on_shutdown() { this->dial_socket_.reset(); }
void dump_config() const;
protected:
enum class DialState : uint8_t {
DIAL_STATE_IDLE,
DIAL_STATE_WAITING,
DIAL_STATE_CONNECTING,
};
static constexpr uint32_t BACKOFF_MIN_MS = 5000;
static constexpr uint32_t BACKOFF_MAX_MS = 300000;
static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000;
static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250;
static constexpr uint32_t NETWORK_RETRY_MS = 500;
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
// Boot waits for the client to connect in first; a deep sleep wake window
// is short, so connecting out immediately is the wake state
#ifdef USE_DEEP_SLEEP
static constexpr uint32_t BOOT_WAIT_MS = 0;
#else
static constexpr uint32_t BOOT_WAIT_MS = API_OUTGOING_CONNECTION_DELAY;
#endif
void try_dial_(APIServer *server, uint32_t now);
void poll_connect_(APIServer *server, uint32_t now);
// Hand the connected socket to the server and gate on the new connection
void handoff_(APIServer *server, uint32_t now);
// Close any half-open dial and wait a jittered backoff before retrying
void schedule_retry_(uint32_t now);
// Wait without escalating the backoff (used for unmet preconditions)
void schedule_wait_(uint32_t now, uint32_t wait);
#ifndef API_OUTGOING_CONNECTION_HOST
// Write saved_ to flash, tracking success in host_persisted_
bool persist_target_() {
this->host_persisted_ = this->target_pref_.save(&this->saved_) && global_preferences->sync();
return this->host_persisted_;
}
#endif
const char *target_host_() const {
#ifdef API_OUTGOING_CONNECTION_HOST
return API_OUTGOING_CONNECTION_HOST;
#else
return this->saved_.host[0] != '\0' ? this->saved_.host : nullptr;
#endif
}
// Pointers first (4 bytes each on 32-bit)
std::unique_ptr<socket::Socket> dial_socket_;
// Compared only, never dereferenced
APIConnection *dialed_conn_{nullptr};
#ifndef API_OUTGOING_CONNECTION_HOST
ESPPreferenceObject target_pref_;
#endif
// 4-byte types
uint32_t backoff_{BACKOFF_MIN_MS};
uint32_t wait_{BOOT_WAIT_MS};
uint32_t state_ts_{0};
uint32_t last_poll_{0};
// Byte-aligned types last
#ifndef API_OUTGOING_CONNECTION_HOST
SavedOutgoingTarget saved_{};
// False while saved_ holds a value the flash write failed for; retried on
// the next flagged hello
bool host_persisted_{false};
#endif
DialState state_{DialState::DIAL_STATE_WAITING};
};
} // namespace esphome::api
#endif // USE_API && USE_API_OUTGOING_CONNECTION
-29
View File
@@ -15,11 +15,6 @@ bool HelloRequest::decode_varint(uint32_t field_id, proto_varint_value_t value)
case 3:
this->api_version_minor = value;
break;
#ifdef USE_API_OUTGOING_CONNECTION
case 4:
this->outgoing_connection_target = value != 0;
break;
#endif
default:
return false;
}
@@ -180,9 +175,6 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
#endif
#ifdef USE_API_NOISE
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
#endif
#ifdef USE_API_OUTGOING_CONNECTION
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 27, this->api_outgoing_connection_supported);
#endif
return pos;
}
@@ -248,9 +240,6 @@ uint32_t DeviceInfoResponse::calculate_size() const {
#endif
#ifdef USE_API_NOISE
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
#endif
#ifdef USE_API_OUTGOING_CONNECTION
size += ProtoSize::calc_bool(2, this->api_outgoing_connection_supported);
#endif
return size;
}
@@ -1286,24 +1275,12 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example);
#endif
return pos;
}
uint32_t ListEntitiesServicesArgument::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->name.size());
size += this->type ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->example.size());
#endif
return size;
}
uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1314,9 +1291,6 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description);
#endif
return pos;
}
uint32_t ListEntitiesServicesResponse::calculate_size() const {
@@ -1329,9 +1303,6 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const {
}
}
size += this->supports_response ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
return size;
}
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+3 -18
View File
@@ -412,16 +412,13 @@ class CommandProtoMessage : public ProtoDecodableMessage {
class HelloRequest final : public ProtoDecodableMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 1;
static constexpr uint8_t ESTIMATED_SIZE = 19;
static constexpr uint8_t ESTIMATED_SIZE = 17;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("hello_request"); }
#endif
StringRef client_info{};
uint32_t api_version_major{0};
uint32_t api_version_minor{0};
#ifdef USE_API_OUTGOING_CONNECTION
bool outgoing_connection_target{false};
#endif
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
@@ -552,7 +549,7 @@ class SerialProxyInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 10;
static constexpr uint16_t ESTIMATED_SIZE = 315;
static constexpr uint16_t ESTIMATED_SIZE = 312;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
#endif
@@ -610,9 +607,6 @@ class DeviceInfoResponse final : public ProtoMessage {
#endif
#ifdef USE_API_NOISE
bool api_encryption_provisionable{false};
#endif
#ifdef USE_API_OUTGOING_CONNECTION
bool api_outgoing_connection_supported{false};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
@@ -1323,12 +1317,6 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1340,7 +1328,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 59;
static constexpr uint8_t ESTIMATED_SIZE = 50;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
@@ -1348,9 +1336,6 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
-15
View File
@@ -885,9 +885,6 @@ const char *HelloRequest::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("client_info"), this->client_info);
dump_field(out, ESPHOME_PSTR("api_version_major"), this->api_version_major);
dump_field(out, ESPHOME_PSTR("api_version_minor"), this->api_version_minor);
#ifdef USE_API_OUTGOING_CONNECTION
dump_field(out, ESPHOME_PSTR("outgoing_connection_target"), this->outgoing_connection_target);
#endif
return out.c_str();
}
const char *HelloResponse::dump_to(DumpBuffer &out) const {
@@ -1011,9 +1008,6 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
#endif
#ifdef USE_API_NOISE
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
#endif
#ifdef USE_API_OUTGOING_CONNECTION
dump_field(out, ESPHOME_PSTR("api_outgoing_connection_supported"), this->api_outgoing_connection_supported);
#endif
return out.c_str();
}
@@ -1506,12 +1500,6 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
@@ -1524,9 +1512,6 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
out.append("\n");
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const {
+49 -139
View File
@@ -34,46 +34,7 @@ APIServer::APIServer() { global_api_server = this; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
}
bool APIServer::create_listen_socket_() {
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->socket_ == nullptr) {
this->socket_failed_(LOG_STR("creation"));
return false;
}
int enable = 1;
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
// we can still continue
}
err = this->socket_->setblocking(false);
if (err != 0) {
this->socket_failed_(LOG_STR("nonblocking"));
return false;
}
struct sockaddr_storage server;
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
if (sl == 0) {
this->socket_failed_(LOG_STR("set sockaddr"));
return false;
}
err = this->socket_->bind((struct sockaddr *) &server, sl);
if (err != 0) {
this->socket_failed_(LOG_STR("bind"));
return false;
}
err = this->socket_->listen(this->listen_backlog_);
if (err != 0) {
this->socket_failed_(LOG_STR("listen"));
return false;
}
return true;
this->mark_failed();
}
void APIServer::setup() {
@@ -92,14 +53,41 @@ void APIServer::setup() {
#endif
#endif
if (!this->create_listen_socket_()) {
#ifdef USE_API_OUTGOING_CONNECTION
// Dial-out needs no listener; degrade instead of stopping the component
this->status_set_error(LOG_STR("listen socket failed"));
#else
this->mark_failed();
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->socket_ == nullptr) {
this->socket_failed_(LOG_STR("creation"));
return;
}
int enable = 1;
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
// we can still continue
}
err = this->socket_->setblocking(false);
if (err != 0) {
this->socket_failed_(LOG_STR("nonblocking"));
return;
}
struct sockaddr_storage server;
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
if (sl == 0) {
this->socket_failed_(LOG_STR("set sockaddr"));
return;
}
err = this->socket_->bind((struct sockaddr *) &server, sl);
if (err != 0) {
this->socket_failed_(LOG_STR("bind"));
return;
}
err = this->socket_->listen(this->listen_backlog_);
if (err != 0) {
this->socket_failed_(LOG_STR("listen"));
return;
#endif
}
#ifdef USE_LOGGER
@@ -147,9 +135,6 @@ void APIServer::setup() {
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
this->status_set_warning(LOG_STR("waiting for client connection"));
}
#ifdef USE_API_OUTGOING_CONNECTION
this->outgoing_conn_.setup();
#endif
}
void APIServer::loop() {
@@ -158,12 +143,6 @@ void APIServer::loop() {
this->accept_new_connections_();
}
#ifdef USE_API_OUTGOING_CONNECTION
if (!this->shutting_down_) {
this->outgoing_conn_.loop(this);
}
#endif
if (this->api_connection_count_ == 0) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time).
@@ -172,12 +151,7 @@ void APIServer::loop() {
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_connected_ > this->reboot_timeout_) {
// Distinguish a wrong-key peer from nothing connecting at all
if (this->saw_unauthenticated_client_) {
ESP_LOGE(TAG, "Clients connected but none authenticated; rebooting");
} else {
ESP_LOGE(TAG, "No clients; rebooting");
}
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
}
@@ -229,15 +203,6 @@ void APIServer::remove_client_(uint8_t client_index) {
std::string client_peername(client->get_peername_to(peername_buf));
#endif
// Read before the swap-and-reset below destroys the connection
const bool was_authenticated = client->is_authenticated();
#ifdef USE_API_OUTGOING_CONNECTION
if (client->flags_.outgoing_connection_target) {
this->outgoing_target_count_--;
}
this->outgoing_conn_.on_client_removed(client.get(), was_authenticated);
#endif
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
@@ -256,18 +221,9 @@ void APIServer::remove_client_(uint8_t client_index) {
// Last client disconnected - set warning and start tracking for reboot timeout
// (suppressed while provisioning is pending - see loop()).
// Refresh on every authenticated removal, not just the last one, so an
// unauthenticated straggler removed later (e.g. a port scan, or a dial to
// a host that accepts TCP but never speaks the API) cannot discard a
// healthy session's timestamp and trigger a spurious reboot
if (was_authenticated) {
this->last_connected_ = App.get_loop_component_start_time();
this->saw_unauthenticated_client_ = false;
} else {
this->saw_unauthenticated_client_ = true;
}
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
@@ -289,7 +245,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->at_client_limit_()) {
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
@@ -298,54 +254,18 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
ESP_LOGD(TAG, "Accept %s", peername);
this->add_client_(new APIConnection(std::move(sock), this));
auto *conn = new APIConnection(std::move(sock), this);
this->clients_[this->api_connection_count_++].reset(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
}
}
bool APIServer::add_client_(APIConnection *conn) {
if (this->at_client_limit_()) {
// The accept path checks first to skip the allocation; the outgoing
// handoff relies on this check
ESP_LOGW(TAG, "Max connections (%d), dropping client", MAX_API_CONNECTIONS);
delete conn;
return false;
}
this->clients_[this->api_connection_count_++].reset(conn);
conn->start();
// First client connected - clear warning. The reboot watchdog timestamp is
// refreshed when an authenticated client is removed (see remove_client_),
// never on bare TCP connects.
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
this->status_clear_warning();
}
return true;
}
#ifdef USE_API_OUTGOING_CONNECTION
APIConnection *APIServer::add_outgoing_client_(std::unique_ptr<socket::Socket> sock) {
// Re-check at the handoff: the PSK may have been cleared since the dial
// started (mark_outgoing() needs the noise helper); add_client_ re-checks
// the slot limit
if (!this->noise_ctx_.has_psk()) {
ESP_LOGW(TAG, "Dropping outgoing connection (no key)");
return nullptr;
}
auto *conn = new APIConnection(std::move(sock), this);
if (!this->add_client_(conn)) {
return nullptr;
}
// After start(): sends our server hello first so the peer can pick the key
conn->mark_outgoing();
return conn;
}
void APIServer::on_outgoing_target_client(APIConnection *conn) {
this->outgoing_target_count_++;
this->outgoing_conn_.on_target_client(conn);
}
#endif
void APIServer::dump_config() {
char addr_buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
@@ -362,9 +282,6 @@ void APIServer::dump_config() {
#else
ESP_LOGCONFIG(TAG, " Noise encryption: NO");
#endif
#ifdef USE_API_OUTGOING_CONNECTION
this->outgoing_conn_.dump_config();
#endif
}
void APIServer::handle_disconnect(APIConnection *conn) {}
@@ -516,10 +433,8 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -659,8 +574,6 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
if (!c->send_message(req)) {
API_LOG_MSG_DROPPED(TAG, "Disconnect request");
}
// Force it: a session from before the key was active must not survive
c->flags_.next_close = true;
}
});
}
@@ -772,9 +685,6 @@ void APIServer::on_shutdown() {
// Close the listening socket to prevent new connections
this->destroy_socket_();
#ifdef USE_API_OUTGOING_CONNECTION
this->outgoing_conn_.on_shutdown();
#endif
// Change batch delay to 5ms for quick flushing during shutdown
this->batch_delay_ = 5;
+1 -28
View File
@@ -11,7 +11,6 @@
#endif
#include "api_pb2.h"
#include "api_pb2_service.h"
#include "api_outgoing_connection.h"
#include "esphome/components/socket/socket.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
@@ -82,10 +81,6 @@ class APIServer final : public Component,
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE
#ifdef USE_API_OUTGOING_CONNECTION
// Called by APIConnection when a client declares itself a dial-back target in its hello
void on_outgoing_target_client(APIConnection *conn);
#endif
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
@@ -263,16 +258,6 @@ class APIServer final : public Component,
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Insert a constructed connection into the client slots and start it.
// Takes ownership; deletes the connection and returns false at the limit
bool add_client_(APIConnection *conn);
bool at_client_limit_() const { return this->api_connection_count_ >= MAX_API_CONNECTIONS; }
#ifdef USE_API_OUTGOING_CONNECTION
// Returns the new connection, or nullptr (socket dropped) when at the limit
APIConnection *add_outgoing_client_(std::unique_ptr<socket::Socket> sock);
bool has_outgoing_target_client_() const { return this->outgoing_target_count_ != 0; }
friend class OutgoingConnectionManager;
#endif
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
void __attribute__((noinline)) remove_client_(uint8_t client_index);
@@ -312,7 +297,6 @@ class APIServer final : public Component,
this->socket_ = nullptr;
}
void socket_failed_(const LogString *msg);
bool create_listen_socket_();
// Pointers and pointer-like types first (4 bytes each)
socket::ListenSocket *socket_{nullptr};
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -365,16 +349,8 @@ class APIServer final : public Component,
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
// Bit-packed so the two flags share one byte
bool shutting_down_ : 1 = false;
// For the reboot log: whether any removal since the last watchdog refresh
// was an unauthenticated session (e.g. a wrong-key peer)
bool saw_unauthenticated_client_ : 1 = false;
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
#ifdef USE_API_OUTGOING_CONNECTION
// Connected clients whose hello declared them a dial-back target
uint8_t outgoing_target_count_{0};
#endif
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
// Index assigned by the provisioning manager for reporting this transport's state.
uint8_t provisioning_source_{0};
@@ -384,9 +360,6 @@ class APIServer final : public Component,
noise::NoiseContext noise_ctx_;
ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE
#ifdef USE_API_OUTGOING_CONNECTION
OutgoingConnectionManager outgoing_conn_;
#endif
};
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
+1 -2
View File
@@ -99,8 +99,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
auto resp = service->encode_list_service_response();
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
-43
View File
@@ -1,52 +1,9 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
}
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
if (arg.legacy_int != 0)
+36 -55
View File
@@ -1,6 +1,5 @@
#pragma once
#include <span>
#include <tuple>
#include <utility>
#include <vector>
@@ -20,9 +19,7 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -37,51 +34,29 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
// the name and every argument is (name, description, example). Unset metadata is nullptr.
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
#else
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
#endif
class UserServiceStatic : public UserServiceDescriptor {
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceStatic(const char *const *strings, uint32_t key,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: strings_(strings), key_(key), supports_response_(supports_response) {}
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
this->key_ = fnv1_hash(name);
}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
enums::SupportsResponseType supports_response_;
};
template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types, scratch);
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -114,6 +89,12 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -125,7 +106,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
@@ -186,8 +167,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
@@ -198,8 +179,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
@@ -212,8 +193,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
@@ -224,8 +205,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
+2 -4
View File
@@ -23,10 +23,8 @@ void AQISensor::setup() {
void AQISensor::dump_config() {
ESP_LOGCONFIG(TAG, "AQI Sensor:");
ESP_LOGCONFIG(TAG, " Calculation Type: %s",
this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI"));
ESP_LOGCONFIG(TAG, " Extended Range: %s",
this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled");
if (this->pm_2_5_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
}
@@ -42,16 +42,7 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
return false;
}
if (socket_->setblocking(false) != 0) {
// Capture before the log and reset() below can clobber errno; a blocking
// connect()/read() would otherwise stall the whole loop
const int saved_errno = errno;
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
socket_.reset();
if (error_cb_)
error_cb_(error_arg_, this, saved_errno);
return false;
}
socket_->setblocking(false);
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
if (err == 0) {
@@ -106,22 +97,45 @@ void AsyncClient::loop() {
return;
if (connecting_) {
int err = 0;
switch (socket::poll_connect(*socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
break;
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
// For connecting, we need to check writability, not readability
// The Application's select() only monitors read FDs, so we do our own check here
// For ESP platforms lwip_select() might be faster, but this code isn't used
// on those platforms anyway. If it was, we'd fix the Application select()
// to report writability instead of doing it this way.
int fd = socket_->get_fd();
if (fd < 0) {
ESP_LOGW(TAG, "Invalid socket fd");
close();
return;
}
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
if (ret > 0 && FD_ISSET(fd, &writefds)) {
int error = 0;
socklen_t len = sizeof(error);
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
connecting_ = false;
connected_ = true;
if (connect_cb_)
connect_cb_(connect_arg_, this);
break;
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
ESP_LOGW(TAG, "Connection failed: %d", err);
} else {
ESP_LOGW(TAG, "Connection failed: %d", error);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
break;
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
const int err = errno;
ESP_LOGE(TAG, "Select error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
+2 -3
View File
@@ -6,7 +6,7 @@ import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
include_builtin_idf_component,
request_http_client,
require_certificate_bundle,
)
import esphome.config_validation as cv
@@ -334,8 +334,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N
async def to_code(config: ConfigType) -> None:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client")
request_http_client()
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
@@ -100,15 +100,13 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
}
this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true;
});
}
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
@@ -162,9 +162,8 @@ void BME680BSECComponent::dump_config() {
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_,
this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"),
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"),
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
-2
View File
@@ -16,13 +16,11 @@ CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DESCRIPTION = "description"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
CONF_GYROSCOPE_ODR = "gyroscope_odr"
CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_HOST = "host"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
+5 -8
View File
@@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() {
bool dir = status & (1 << 21);
if (current_gain_ < 0)
dir = !dir;
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
clear |= 1 << 22;
}
@@ -319,9 +319,7 @@ void CS5460AComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"CS5460A:\n"
" Init status: %s",
state == COMPONENT_STATE_LOOP
? LOG_STR_LITERAL("OK")
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
LOG_PIN(" CS Pin: ", cs_);
ESP_LOGCONFIG(TAG,
" Samples / cycle: %" PRIu32 "\n"
@@ -332,10 +330,9 @@ void CS5460AComponent::dump_config() {
" Current HPF: %s\n"
" Voltage HPF: %s\n"
" Pulse energy: %.2f Wh",
samples_, phase_offset_,
pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_,
voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_);
samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_,
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
pulse_energy_wh_);
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
LOG_SENSOR(" ", "Current", current_sensor_);
LOG_SENSOR(" ", "Power", power_sensor_);
+2 -2
View File
@@ -20,8 +20,8 @@ void DHT::dump_config() {
"DHT:\n"
" %sModel: %s\n"
" Internal pull-up: %s",
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_PIN(" Pin: ", this->t_pin_);
LOG_UPDATE_INTERVAL(this);
+1 -1
View File
@@ -93,7 +93,7 @@ void Emc2101Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM");
if (this->dac_mode_) {
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status)
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
bool ENS210Component::set_low_power_(bool enable) {
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
delay(ENS210_BOOTING_MS);
return result;
+148 -56
View File
@@ -73,6 +73,7 @@ from .const import (
KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_IDF_VERSION,
KEY_MBEDTLS_SDKCONFIG,
KEY_NETWORK_SDKCONFIG,
KEY_PATH,
KEY_REF,
@@ -218,7 +219,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf
"esp-tls", # TLS wrapper - re-included by request_tls()
"esp_adc", # ADC driver - only needed by adc component
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
@@ -246,7 +247,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
@@ -740,6 +741,48 @@ def request_software_coexistence() -> None:
include_builtin_idf_component("esp_coex")
@dataclass
class MbedtlsSdkconfigData:
"""Inputs for the mbedTLS sdkconfig flags, reconciled at FINAL.
Components call request_tls() / require_mbedtls_*() instead of writing the
CONFIG_MBEDTLS_* flags directly; _reconcile_mbedtls_sdkconfig() decides the
final values once every to_code has run.
"""
tls_required: bool = False # TLS/DTLS handshake user
ecp_required: bool = False # ECDH/ECDSA without TLS (openthread SRP host key)
peer_cert_required: bool = False # keep the peer certificate after the handshake
pkcs7_required: bool = False # PKCS#7 parsing
sha512_required: bool = False # SHA-384/SHA-512
# esp32 advanced disable_mbedtls_peer_cert / disable_mbedtls_pkcs7 options
disable_peer_cert: bool = True
disable_pkcs7: bool = True
def _mbedtls_sdkconfig() -> MbedtlsSdkconfigData:
data = CORE.data[KEY_ESP32]
if KEY_MBEDTLS_SDKCONFIG not in data:
data[KEY_MBEDTLS_SDKCONFIG] = MbedtlsSdkconfigData()
return data[KEY_MBEDTLS_SDKCONFIG]
def request_tls() -> None:
"""Request the mbedTLS TLS stack and the esp-tls wrapper.
Without a request TLS and its ECP/PEM-write/CRL/CSR crypto compile out;
hashes, AES and RSA stay available.
"""
_mbedtls_sdkconfig().tls_required = True
include_builtin_idf_component("esp-tls")
def request_http_client() -> None:
"""Request ESP-IDF's HTTP client; it links esp_tls even for plain http."""
include_builtin_idf_component("esp_http_client")
request_tls()
def add_idf_component(
*,
name: str,
@@ -1737,10 +1780,7 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required"
@@ -1779,6 +1819,8 @@ def require_certificate_bundle() -> None:
certificates (http_request, audio streaming) call this so the bundle is
compiled and gen_crt_bundle runs only when something uses it.
"""
# esp_crt_bundle.c calls mbedtls_ssl_conf_*, so a bundle always needs TLS.
request_tls()
CORE.data[KEY_ESP32][KEY_CERT_BUNDLE] = True
@@ -1804,33 +1846,32 @@ def require_usb_serial_jtag_secondary() -> None:
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
def require_mbedtls_peer_cert() -> None:
"""Mark that mbedTLS peer certificate retention is required by a component.
def require_mbedtls_ecp() -> None:
"""Keep mbedTLS elliptic curve support (ECDH/ECDSA) without requesting TLS.
Call this from components that need access to the peer certificate after
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
from being disabled.
Call this from components that sign or verify with ECDSA outside a TLS
handshake (openthread's SRP host key). WiFi, Bluetooth and secure boot
select it through Kconfig on their own.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
_mbedtls_sdkconfig().ecp_required = True
def require_mbedtls_peer_cert() -> None:
"""Keep the peer certificate after the TLS handshake (CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE).
A user sdkconfig_options value takes precedence.
"""
_mbedtls_sdkconfig().peer_cert_required = True
def require_mbedtls_pkcs7() -> None:
"""Mark that mbedTLS PKCS#7 support is required by a component.
Call this from components that need PKCS#7 certificate validation.
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
"""Keep mbedTLS PKCS#7 support (CONFIG_MBEDTLS_PKCS7_C). A user sdkconfig_options value takes precedence."""
_mbedtls_sdkconfig().pkcs7_required = True
def require_mbedtls_sha512() -> None:
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
Call this from components that need to verify TLS certificates or signatures
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
and CONFIG_MBEDTLS_SHA512_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
"""Keep mbedTLS SHA-384/SHA-512 (CONFIG_MBEDTLS_SHA384_C / CONFIG_MBEDTLS_SHA512_C)."""
_mbedtls_sdkconfig().sha512_required = True
def idf_version() -> cv.Version:
@@ -2295,6 +2336,81 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
# User sdkconfig_options that mean "keep TLS on" when set to y.
_MBEDTLS_TLS_ON_OPTIONS = (
"CONFIG_MBEDTLS_TLS_ENABLED",
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
)
# Any user option under these prefixes only makes sense with TLS compiled in.
_TLS_OPTION_PREFIXES = ("CONFIG_ESP_TLS_", "CONFIG_MBEDTLS_SSL_", "CONFIG_ESP_HTTPS_")
def _user_sdkconfig_wants_tls(options: dict[str, Any]) -> bool:
"""True when sdkconfig_options turn TLS on or tune something under it; an `n` is never a request."""
return any(
(name in _MBEDTLS_TLS_ON_OPTIONS and value == "y")
or (name == "CONFIG_MBEDTLS_TLS_DISABLED" and value == "n")
or (name.startswith(_TLS_OPTION_PREFIXES) and value != "n")
for name, value in options.items()
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_mbedtls_sdkconfig() -> None:
"""Reconcile the mbedTLS sdkconfig flags after every request_tls() / require_mbedtls_*() call.
mbedtls cannot be excluded from an IDF build (bootloader_support needs its
SHA-256), but with no TLS user the ssl_*.c sources and the TLS-only crypto
compile to empty objects. User sdkconfig_options win.
"""
data = _mbedtls_sdkconfig()
idf6 = idf_version() >= cv.Version(6, 0, 0)
# A component that re-includes esp-tls on its own (external components
# predating request_tls()) wants TLS just as much as a request_tls() call.
tls_required = (
data.tls_required
or "esp-tls" not in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
)
if not CORE.using_arduino and not tls_required:
# IDF 6 made CONFIG_MBEDTLS_TLS_ENABLED a normal bool; on IDF 5 it has
# no prompt and is only reachable through the "None" TLS role choice.
if idf6:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_ENABLED", False)
else:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_DISABLED", True)
# Enterprise WiFi selects TLS back on; wifi writes this itself, but
# esp_wifi can also be in the build without a wifi: block (openthread).
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
# WiFi (ESP_WIFI_MBEDTLS_CRYPTO), Bluetooth and signed apps
# (SECURE_SIGNED_APPS) select ECP back on through Kconfig.
if not data.ecp_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_ECP_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PEM_WRITE_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CRL_PARSE_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CSR_PARSE_C", False)
# Keeping the peer certificate costs ~4KB heap per connection.
if data.peer_cert_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif data.disable_peer_cert:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
if data.pkcs7_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", True)
elif data.disable_pkcs7:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", False)
# SHA-384 shares the SHA-512 compression function, so both go together.
# Only IDF 6.0's PSA engine links a ~3KB software fallback for them; on
# IDF 5 they are a single hardware-only option with no code size cost.
if idf6 and not data.sha512_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA384_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA512_C", False)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_network_sdkconfig() -> None:
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
@@ -2949,38 +3065,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
# Disable keeping peer certificate after TLS handshake
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
# Components that need it can call require_mbedtls_peer_cert()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
# Disable PKCS#7 support in mbedTLS
# Only needed for specific certificate validation scenarios
# Components that need it can call require_mbedtls_pkcs7()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable SHA-384 and SHA-512 in mbedTLS
# ESPHome doesn't use either algorithm. SHA-384 shares the same
# compression function as SHA-512 (mbedtls_internal_sha512_process),
# so both must be disabled to eliminate the ~3KB software fallback
# that IDF 6.0's PSA parallel engine always links in.
# On IDF < 6.0 these are a single config and hardware-only (no
# software fallback), so there was no code size cost to leaving
# them enabled.
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
KEY_MBEDTLS_SHA512_REQUIRED, False
):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
CORE.add_job(_set_libc_picolibc_newlib_compat)
@@ -2990,6 +3074,12 @@ async def to_code(config):
# FINAL priority: runs after every require_certificate_bundle() call
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
# FINAL priority: runs after every request_tls() / require_mbedtls_*() call
mbedtls = _mbedtls_sdkconfig()
mbedtls.disable_peer_cert = advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]
mbedtls.disable_pkcs7 = advanced[CONF_DISABLE_MBEDTLS_PKCS7]
CORE.add_job(_reconcile_mbedtls_sdkconfig)
# FINAL: require_*() calls can come from to_code at or below this priority, so an
# inline read would be iteration-order-dependent; reconcile once after every job ran.
CORE.add_job(
@@ -3021,6 +3111,8 @@ async def to_code(config):
# so it still gets the CMN variant pinned.
if conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE") == "y":
require_certificate_bundle()
if _user_sdkconfig_wants_tls(conf[CONF_SDKCONFIG_OPTIONS]):
request_tls()
# Components from YAML are added in a separate coroutine with FINAL priority
# Schedule it to run after all other components
+1
View File
@@ -30,6 +30,7 @@ KEY_EXTRA_BUILD_FILES = "extra_build_files"
KEY_CERT_BUNDLE = "cert_bundle"
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
KEY_NETWORK_SDKCONFIG = "network_sdkconfig"
KEY_MBEDTLS_SDKCONFIG = "mbedtls_sdkconfig"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32C2 = "ESP32C2"
+4 -13
View File
@@ -5,14 +5,11 @@ import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# Per the ESP32-S31 IDF DOCS and datasheet:
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
# Per the ESP32-S31 datasheet (page 96):
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
@@ -22,8 +19,6 @@ _LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
@@ -38,10 +33,6 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
@@ -128,8 +128,7 @@ void ESPHomeOTAComponent::dump_config() {
esp_partition_iterator_release(it);
esp_bootloader_desc_t bootloader_desc;
esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
#endif // USE_ESP32
#endif // USE_OTA_PARTITIONS
}
@@ -358,10 +357,7 @@ void ESPHomeOTAComponent::handle_data_() {
tv.tv_usec = 0;
this->client_->setsockopt(SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
this->client_->setsockopt(SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv));
if (this->client_->setblocking(true) != 0) {
this->log_socket_error_(LOG_STR("blocking"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
this->client_->setblocking(true);
// Acknowledge auth OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
+7 -10
View File
@@ -93,8 +93,7 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback)
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
@@ -107,8 +106,7 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
@@ -146,8 +144,7 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
@@ -369,7 +366,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
return;
}
#endif
@@ -386,9 +383,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
trig->trigger();
}
}
@@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() {
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"),
this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None"));
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {
@@ -35,20 +35,18 @@ void GraphicalDisplayMenu::setup() {
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(
TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"),
YESNO(this->active_));
ESP_LOGCONFIG(TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
@@ -69,12 +69,10 @@ void GT911Touchscreen::setup_internal_() {
// Direct MCU pin: attach a hardware interrupt, no polling needed.
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
} else {
// IO expander pin: leave as output for configuration only.
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
}
}
}
+1 -2
View File
@@ -248,8 +248,7 @@ void HaierClimateBase::setup() {
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s",
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
}
void HaierClimateBase::loop() {
+5 -5
View File
@@ -343,11 +343,11 @@ void HonClimate::dump_config() {
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""),
(this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""),
(this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""),
(this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""),
(this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : ""));
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
+1 -1
View File
@@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"HDC302x:\n"
" Heater: %s",
this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive"));
this->heater_active_ ? "active" : "inactive");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
+4 -4
View File
@@ -59,7 +59,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
if (this->last_command_ == operation) {
float dur = (float) (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
}
this->publish_state();
}
@@ -213,9 +213,9 @@ void HE60rCover::start_direction_(CoverOperation dir) {
if (this->current_operation == dir)
return;
ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
if (dir == this->next_direction_) {
// either moving and needs to stop, or stopped and will move correctly on one trigger
+1 -2
View File
@@ -336,8 +336,7 @@ void HlkFm22xComponent::dump_config() {
}
if (this->enrolling_binary_sensor_) {
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s",
this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
}
if (this->face_count_sensor_) {
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
+1 -2
View File
@@ -97,8 +97,7 @@ void HLW8012Component::update() {
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode",
this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE"));
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
this->change_mode_at_ = 0;
this->sel_pin_->digital_write(this->current_mode_);
}
+1 -5
View File
@@ -202,11 +202,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_watchdog_timeout(timeout_ms))
if CORE.is_esp32:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time).
# esp-tls is re-enabled too because http_request includes <esp_tls.h>
# directly and esp_http_client only pulls it in as a private dependency.
esp32.include_builtin_idf_component("esp_http_client")
esp32.include_builtin_idf_component("esp-tls")
esp32.request_http_client()
cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX]))
cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX]))
@@ -116,8 +116,8 @@ void ILI9XXXDisplay::dump_config() {
" Mirror_x: %s\n"
" Mirror_y: %s\n"
" Invert colors: %s",
this->color_order_ == display::COLOR_ORDER_BGR ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"),
YESNO(this->swap_xy_), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB", YESNO(this->swap_xy_),
YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
if (this->is_failed()) {
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
+1 -1
View File
@@ -16,7 +16,7 @@ from esphome.types import ConfigType
AUTO_LOAD = ["improv_base"]
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["logger", "network"]
DEPENDENCIES = ["logger", "wifi"]
improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
@@ -1,5 +1,5 @@
#include "improv_serial_component.h"
#ifdef USE_IMPROV_SERIAL
#ifdef USE_WIFI
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
@@ -7,10 +7,7 @@
#include "esphome/core/version.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/network/util.h"
#ifdef USE_WIFI
#include "esphome/components/wifi/scan_list.h"
#endif
#include <array>
@@ -29,17 +26,13 @@ void ImprovSerialComponent::setup() {
this->hw_serial_ = logger::global_logger->get_hw_serial();
#endif
// The Improv state machine tracks Wi-Fi provisioning only. General device
// connectivity (e.g. Ethernet) is reported separately via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->has_sta()) {
if (wifi::global_wifi_component->has_sta()) {
this->state_ = improv::STATE_PROVISIONED;
} else if (wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled()) {
} else if (!wifi::global_wifi_component->is_disabled()) {
// Respect Wi-Fi's disabled state; forcing a scan while disabled throws
// the wifi component into an invalid state from which it cannot recover.
wifi::global_wifi_component->start_scanning();
}
#endif
}
void ImprovSerialComponent::loop() {
@@ -62,14 +55,8 @@ void ImprovSerialComponent::loop() {
}
}
#ifdef USE_WIFI
if (this->state_ == improv::STATE_PROVISIONING && wifi::global_wifi_component != nullptr &&
wifi::global_wifi_component->is_connected()) {
// Being connected is not enough: re-provisioning a device that is already online leaves the
// prior network up until it drops, so check that the joined network is the requested one
// before reporting success. Same test as the wifi.connect action.
char ssid_buf[wifi::SSID_BUFFER_SIZE];
if (strcmp(wifi::global_wifi_component->wifi_ssid_to(ssid_buf), this->connecting_sta_.get_ssid().c_str()) == 0) {
if (this->state_ == improv::STATE_PROVISIONING) {
if (wifi::global_wifi_component->is_connected()) {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
@@ -79,7 +66,6 @@ void ImprovSerialComponent::loop() {
this->send_settings_response_(improv::WIFI_SETTINGS);
}
}
#endif
}
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
@@ -157,17 +143,15 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
#endif
}
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first) {
// The webserver listens on every interface, so advertise each one that has a usable IPv4.
// network::get_ip_addresses() can't be used here: it returns only the highest-priority
// interface's addresses, which are all-unset (0.0.0.0) when e.g. Ethernet has no link while
// the device is online via Wi-Fi, and 0.0.0.0 must not become the advertised URL. OpenThread
// is omitted: it only ever has IPv6 addresses, which cannot form an IPv4 http:// URL.
const auto append_urls = [&builder](const network::IPAddresses &addresses) {
for (const auto &ip : addresses) {
if (!ip.is_ip4() || !ip.is_set())
continue;
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
if (ip.is_ip4()) {
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
ip.str_to(ip_buf);
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
@@ -178,43 +162,9 @@ void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &buil
if (!builder.add_string(webserver_url, len)) {
ESP_LOGW(TAG, "Response full; URL dropped");
}
break;
}
};
#ifdef USE_WIFI
// Clients redirect to the first URL, so the interface the client just configured has to lead:
// another interface's address can be on a subnet that client cannot reach.
const auto append_wifi_urls = [&append_urls]() {
if (wifi::global_wifi_component != nullptr)
append_urls(wifi::global_wifi_component->get_ip_addresses());
};
if (wifi_first)
append_wifi_urls();
#endif
#ifdef USE_ETHERNET
if (ethernet::global_eth_component != nullptr)
append_urls(ethernet::global_eth_component->get_ip_addresses());
#endif
#ifdef USE_MODEM
if (modem::global_modem_component != nullptr)
append_urls(modem::global_modem_component->get_ip_addresses());
#endif
#ifdef USE_WIFI
if (!wifi_first)
append_wifi_urls();
#endif
}
#endif // USE_WEBSERVER
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, command);
#ifdef USE_IMPROV_SERIAL_NEXT_URL
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
#endif
#ifdef USE_WEBSERVER
// This response only ever answers Wi-Fi provisioning, so lead with the Wi-Fi URL as it did
// before other interfaces were reported.
this->add_webserver_urls_(builder, /*wifi_first=*/true);
}
#endif
this->send_response_(builder.finish(false));
}
@@ -281,8 +231,7 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
switch (command.command) {
case improv::WIFI_SETTINGS: {
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
if (wifi::global_wifi_component->is_disabled()) {
// Wi-Fi is disabled, so we can't provision. Respond immediately
// instead of letting the client wait out its provisioning timeout.
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
@@ -294,32 +243,21 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
sta.set_password(command.password.c_str());
this->connecting_sta_ = sta;
// Sampled before start_connecting(): the old connection drops asynchronously after it.
const bool switching = wifi::global_wifi_component->is_connected();
wifi::global_wifi_component->set_sta(sta);
wifi::global_wifi_component->start_connecting(sta);
this->set_state_(improv::STATE_PROVISIONING);
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
[this]() { this->on_wifi_connect_timeout_(); });
#else
// No Wi-Fi support compiled in; there is nothing to provision.
ESP_LOGW(TAG, "Wi-Fi not supported; cannot provision");
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
#endif
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
return true;
}
case improv::GET_CURRENT_STATE: {
// This state machine tracks Wi-Fi provisioning only. When Wi-Fi is disabled or not
// compiled in, provisioning is unavailable -> report STOPPED so the client doesn't
// offer a Wi-Fi form. General connectivity (e.g. Ethernet) is reported separately
// via GET_NETWORK_STATE.
#ifdef USE_WIFI
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
// Reported transiently without disturbing our internal provisioning state machine,
// so a later `wifi.enable` still reports the correct state.
case improv::GET_CURRENT_STATE:
if (wifi::global_wifi_component->is_disabled()) {
// Wi-Fi is disabled; report the Improv "stopped" state so a client can tell
// the user that provisioning is unavailable. Reported transiently without
// disturbing our internal provisioning state machine, so a later `wifi.enable`
// still reports the correct state.
this->send_current_state_(improv::STATE_STOPPED);
return true;
}
@@ -327,20 +265,14 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
if (this->state_ == improv::STATE_PROVISIONED) {
this->send_settings_response_(improv::GET_CURRENT_STATE);
}
#else
this->send_current_state_(improv::STATE_STOPPED);
#endif
return true;
}
case improv::GET_DEVICE_INFO: {
this->send_version_info_();
return true;
}
case improv::GET_WIFI_NETWORKS: {
// Declared out here because the terminating empty response is sent with or without Wi-Fi
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
#ifdef USE_WIFI
const auto &results = wifi::global_wifi_component->get_scan_result();
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
for (const auto &scan : results) {
bool with_auth = false;
if (!wifi::should_show_scan_entry(results, scan, with_auth))
@@ -357,52 +289,11 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
builder.add_string(YESNO(with_auth));
this->send_response_(builder.finish(false));
}
#endif // USE_WIFI
// Send empty response to signify the end of the list.
improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS);
this->send_response_(builder.finish(false));
return true;
}
case improv::GET_NETWORK_STATE: {
// Reports general device connectivity and which network interfaces are present, decoupled
// from the Wi-Fi-only provisioning state machine. data[0] is a decimal flags byte;
// when online, the reachable device URL(s) follow.
uint8_t flags = 0;
if (network::is_connected())
flags |= improv::NETWORK_IS_ONLINE;
#ifdef USE_WIFI
flags |= improv::NETWORK_SUPPORTS_WIFI;
#endif
#ifdef USE_ETHERNET
flags |= improv::NETWORK_SUPPORTS_ETHERNET;
#endif
#ifdef USE_OPENTHREAD
flags |= improv::NETWORK_SUPPORTS_THREAD;
#endif
#ifdef USE_MODEM
flags |= improv::NETWORK_SUPPORTS_MODEM;
#endif
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
improv::RpcResponseBuilder builder(buf, improv::GET_NETWORK_STATE);
// Every flag bit fits int8_t's positive range, so int8_to_str renders the byte
static_assert(improv::NETWORK_SUPPORTS_MODEM <= 0x7F, "network flags no longer fit int8_to_str");
char flags_buf[4]; // uint8_t: max "255" + null
char *flags_end = int8_to_str(flags_buf, static_cast<int8_t>(flags));
builder.add_string(flags_buf, flags_end - flags_buf);
#ifdef USE_WEBSERVER
// Not tied to one interface, so follow the configured priority the way
// network::get_ip_addresses() does: a wifi-first network priority list leads with Wi-Fi.
if (flags & improv::NETWORK_IS_ONLINE) {
#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI)
this->add_webserver_urls_(builder, /*wifi_first=*/true);
#else
this->add_webserver_urls_(builder, /*wifi_first=*/false);
#endif
}
#endif
this->send_response_(builder.finish(false));
return true;
}
default: {
ESP_LOGW(TAG, "Unknown payload");
this->set_error_(improv::ERROR_UNKNOWN_RPC);
@@ -440,14 +331,12 @@ void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
this->write_data_(response.data(), response.size());
}
#ifdef USE_WIFI
void ImprovSerialComponent::on_wifi_connect_timeout_() {
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
this->set_state_(improv::STATE_AUTHORIZED);
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
wifi::global_wifi_component->clear_sta();
}
#endif
ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -2,19 +2,15 @@
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/network/util.h"
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_IMPROV_SERIAL
#ifdef USE_WIFI
#include <improv.h>
#include <span>
#include <vector>
#ifdef USE_WIFI
#include "esphome/components/wifi/wifi_component.h"
#endif
#ifdef USE_IMPROV_SERIAL_UART
#include "esphome/components/uart/uart_component.h"
#elif defined(USE_ESP32)
@@ -52,22 +48,13 @@ enum ImprovSerialType : uint8_t {
static const uint16_t IMPROV_SERIAL_TIMEOUT = 100;
static const uint8_t IMPROV_SERIAL_VERSION = 1;
#ifdef USE_WIFI
// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while
// switching networks on an already-connected device (disconnect + reconnect) can legitimately
// take longer; 90 s matches esp32_improv's default wifi_timeout.
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
#endif
// The serial frame length field is one byte
static constexpr size_t MAX_SERIAL_RESPONSE = 255;
// command + data length + trailing byte
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD;
#ifdef USE_WEBSERVER
// length byte + "http://" + IPv4 + ":" + port. Reserves the first URL only; a device with
// several interfaces online adds the rest best-effort and warns if one no longer fits.
// length byte + "http://" + IPv4 + ":" + port
static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5;
#else
static constexpr size_t WEBSERVER_URL_RESERVE = 0;
@@ -97,15 +84,8 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
void send_current_state_(improv::State state);
void set_error_(improv::Error error);
void send_response_(std::span<const uint8_t> response);
#ifdef USE_WIFI
void on_wifi_connect_timeout_();
#endif
#ifdef USE_WEBSERVER
/// Append one web server URL per interface that has a usable IPv4. With wifi_first the Wi-Fi
/// URL leads, for responses to Wi-Fi provisioning; otherwise interfaces go in priority order.
void add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first);
#endif
void send_settings_response_(improv::Command command);
void send_version_info_();
@@ -187,9 +167,7 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
std::vector<uint8_t> rx_buffer_;
uint32_t last_read_byte_{0};
#ifdef USE_WIFI
wifi::WiFiAP connecting_sta_;
#endif
improv::State state_{improv::STATE_AUTHORIZED};
};
@@ -209,8 +209,7 @@ void INA2XX::dump_config() {
" CURRENT_LSB = %f\n"
" SHUNT_CAL = %d",
this->shunt_resistance_ohm_, this->max_current_a_, this->shunt_tempco_ppm_c_,
(uint8_t) this->adc_range_,
this->adc_range_ ? LOG_STR_LITERAL("±40.96 mV") : LOG_STR_LITERAL("±163.84 mV"), this->current_lsb_,
(uint8_t) this->adc_range_, this->adc_range_ ? "±40.96 mV" : "±163.84 mV", this->current_lsb_,
this->shunt_cal_);
ESP_LOGCONFIG(TAG, " ADC Samples = %d; ADC times: Bus = %d μs, Shunt = %d μs, Temp = %d μs",
@@ -1,7 +1,5 @@
import esphome.codegen as cg
from esphome.components import sensor
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
from esphome.components.esp32.const import VARIANT_ESP32
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
@@ -50,10 +48,6 @@ async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32:
# temprature_sens_read() lives in the esp_phy blob, which is excluded by default
include_builtin_idf_component("esp_phy")
if CORE.using_zephyr and CORE.is_nrf52:
zephyr_add_prj_conf("SENSOR", True)
zephyr_add_prj_conf("TEMP_NRF5", True)
+20 -22
View File
@@ -740,7 +740,7 @@ bool IT8951Display::prepare_update_region_(UpdateMode &mode) {
this->reset_dirty_region_();
ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast<unsigned>(mode),
this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono"));
this->grayscale_ ? "grayscale" : "mono");
return true;
}
@@ -1063,27 +1063,25 @@ void IT8951Display::dump_config() {
strncpy(force_temperature, "(controller default)", sizeof(force_temperature));
force_temperature[sizeof(force_temperature) - 1] = '\0';
}
ESP_LOGCONFIG(
TAG,
" Model preset: %s"
"\n Dimensions: %dx%d"
"\n Buffer: %u bytes"
"\n Image buffer addr: 0x%04X%04X"
"\n VCOM: %.02fV (set selector 0x%04X)"
"\n Force temperature: %s"
"\n Display command: %s"
"\n Sleep when done: %s"
"\n Full update every: %u"
"\n Inverted colors: %s"
"\n Pixel format: %s"
"\n Reset duration: %" PRIu32 "ms",
this->name_ != nullptr ? this->name_ : LOG_STR_LITERAL("(unknown)"), this->get_width_internal(),
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_, force_temperature,
this->use_legacy_dpy_area_ ? LOG_STR_LITERAL("DPY_AREA (0x0034, legacy)")
: LOG_STR_LITERAL("DPY_BUF_AREA (0x0037)"),
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
this->grayscale_ ? LOG_STR_LITERAL("4bpp grayscale") : LOG_STR_LITERAL("1bpp monochrome"), this->reset_duration_);
ESP_LOGCONFIG(TAG,
" Model preset: %s"
"\n Dimensions: %dx%d"
"\n Buffer: %u bytes"
"\n Image buffer addr: 0x%04X%04X"
"\n VCOM: %.02fV (set selector 0x%04X)"
"\n Force temperature: %s"
"\n Display command: %s"
"\n Sleep when done: %s"
"\n Full update every: %u"
"\n Inverted colors: %s"
"\n Pixel format: %s"
"\n Reset duration: %" PRIu32 "ms",
this->name_ != nullptr ? this->name_ : "(unknown)", this->get_width_internal(),
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_,
force_temperature, this->use_legacy_dpy_area_ ? "DPY_AREA (0x0034, legacy)" : "DPY_BUF_AREA (0x0037)",
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
this->grayscale_ ? "4bpp grayscale" : "1bpp monochrome", this->reset_duration_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_PIN(" CS Pin: ", this->cs_);
+1 -2
View File
@@ -150,8 +150,7 @@ void Lc709203f::dump_config() {
" Pack Size: %d mAH\n"
" Pack APA: 0x%02X\n"
" Pack Rated Voltage: 3.%sV",
this->pack_size_, this->apa_,
this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7"));
this->pack_size_, this->apa_, this->pack_voltage_ == 0x0000 ? "8" : "7");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
+1 -2
View File
@@ -701,8 +701,7 @@ uint8_t LD2420Component::set_config_mode(bool enable) {
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
}
cmd_frame.footer = CMD_FRAME_FOOTER;
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
cmd_frame.command);
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
return this->send_cmd_from_array(cmd_frame);
}
+1 -2
View File
@@ -437,8 +437,7 @@ void LD6002BComponent::dump_config() {
"HLK-LD6002B:\n"
" Auto wake: %s\n"
" Max data length: %u",
this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
static_cast<unsigned>(this->max_data_len_));
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
if (this->wakeup_pin_ != nullptr) {
LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
+5 -2
View File
@@ -483,7 +483,6 @@ LV_ANIM = LvConstant(
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
LV_GRAD_EXTEND = LvConstant("LV_GRAD_EXTEND_", "PAD", "REPEAT", "REFLECT")
LV_LOG_LEVELS = {
"VERBOSE": "TRACE",
@@ -628,6 +627,10 @@ OBJ_FLAGS = (
"send_draw_task_events",
"widget_1",
"widget_2",
"user_1",
"user_2",
"user_3",
"user_4",
)
LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS)
@@ -901,7 +904,7 @@ LV_COLOR_FORMATS = (
LV_DEFINES = (
"LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_SW_COMPLEX_GRADIENTS", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
"LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP",
"LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE",
"LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL",
+23 -172
View File
@@ -13,40 +13,18 @@ from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import (
CONF_END_ANGLE,
CONF_GRADIENTS,
CONF_OPA,
CONF_START_ANGLE,
LV_DITHER,
LV_GRAD_EXTEND,
add_define,
add_lv_use,
add_warning,
)
from .lv_validation import (
lv_angle_degrees,
lv_color,
lv_percentage,
opacity,
pixels_or_percent,
)
from .lv_validation import lv_color, lv_percentage, opacity
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
CONF_STOPS = "stops"
CONF_LINEAR = "linear"
CONF_RADIAL = "radial"
CONF_CONICAL = "conical"
CONF_EXTEND = "extend"
CONF_FROM_X = "from_x"
CONF_FROM_Y = "from_y"
CONF_TO_X = "to_x"
CONF_TO_Y = "to_y"
CONF_CENTER_X = "center_x"
CONF_CENTER_Y = "center_y"
CONF_FOCAL_X = "focal_x"
CONF_FOCAL_Y = "focal_y"
CONF_FOCAL_RADIUS = "focal_radius"
def min_stops(value):
@@ -55,109 +33,27 @@ def min_stops(value):
return value
STOPS_SCHEMA = cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
)
LINEAR_SCHEMA = cv.Schema(
{
cv.Required(CONF_FROM_X): pixels_or_percent,
cv.Required(CONF_FROM_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
RADIAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Required(CONF_TO_X): pixels_or_percent,
cv.Required(CONF_TO_Y): pixels_or_percent,
cv.Optional(CONF_FOCAL_X): pixels_or_percent,
cv.Optional(CONF_FOCAL_Y): pixels_or_percent,
# No default: gradient_validator() must be able to tell whether this was actually
# given, to require it alongside focal_x/focal_y rather than silently drop it.
# LVGL's lv_grad_radial_set_focal() takes this as a scalar, not lv_pct() -
# unlike every other coordinate here, a percentage is not accepted.
cv.Optional(CONF_FOCAL_RADIUS): cv.positive_int,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
CONICAL_SCHEMA = cv.Schema(
{
cv.Required(CONF_CENTER_X): pixels_or_percent,
cv.Required(CONF_CENTER_Y): pixels_or_percent,
cv.Optional(CONF_START_ANGLE, default=0): lv_angle_degrees,
cv.Optional(CONF_END_ANGLE, default=360): lv_angle_degrees,
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
}
)
def gradient_validator(config):
direction = config[CONF_DIRECTION]
for gradient_direction, key in (
("LINEAR", CONF_LINEAR),
("RADIAL", CONF_RADIAL),
("CONICAL", CONF_CONICAL),
):
if direction == gradient_direction:
if key not in config:
raise cv.Invalid(
f"'{key}' is required for {gradient_direction} gradient direction"
)
elif key in config:
raise cv.Invalid(
f"'{key}' is only valid with 'direction: {gradient_direction}'"
)
if CONF_RADIAL in config:
radial = config[CONF_RADIAL]
has_focal_x = CONF_FOCAL_X in radial
has_focal_y = CONF_FOCAL_Y in radial
has_focal_radius = CONF_FOCAL_RADIUS in radial
if has_focal_x != has_focal_y or (has_focal_radius and not has_focal_x):
raise cv.Invalid(
"'focal_x', 'focal_y' and 'focal_radius' must be specified together "
"in 'radial'"
)
return config
GRADIENT_SCHEMA = cv.ensure_list(
cv.All(
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR",
"HORIZONTAL",
"VER",
"VERTICAL",
"LINEAR",
"RADIAL",
"CONICAL",
upper=True,
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Optional(CONF_LINEAR): LINEAR_SCHEMA,
cv.Optional(CONF_RADIAL): RADIAL_SCHEMA,
cv.Optional(CONF_CONICAL): CONICAL_SCHEMA,
cv.Required(CONF_STOPS): STOPS_SCHEMA,
}
),
gradient_validator,
cv.Schema(
{
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
cv.Required(CONF_DIRECTION): cv.one_of(
"HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True
),
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
cv.Required(CONF_STOPS): cv.All(
[
cv.Schema(
{
cv.Required(CONF_COLOR): lv_color,
cv.Optional(CONF_OPA, default=1.0): opacity,
cv.Required(CONF_POSITION): lv_percentage,
}
)
],
min_stops,
),
}
)
)
@@ -169,60 +65,15 @@ async def gradients_to_code(config):
add_warning(
"The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored"
)
if any(
x[CONF_DIRECTION] in ("LINEAR", "RADIAL", "CONICAL")
for x in config.get(CONF_GRADIENTS, ())
):
# LVGL's software renderer only draws these gradient types when this is enabled; without
# it they silently fall back to a plain horizontal gradient.
add_define("LV_USE_DRAW_SW_COMPLEX_GRADIENTS")
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
idbase = gradient[CONF_ID].id
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
max_stops = max(max_stops, len(stops))
direction = gradient[CONF_DIRECTION]
if direction.startswith("VER"):
if gradient[CONF_DIRECTION].startswith("VER"):
lv.grad_vertical_init(var)
elif direction.startswith("HOR"):
else:
lv.grad_horizontal_init(var)
elif direction == "LINEAR":
linear = gradient[CONF_LINEAR]
lv.grad_linear_init(
var,
await pixels_or_percent.process(linear[CONF_FROM_X]),
await pixels_or_percent.process(linear[CONF_FROM_Y]),
await pixels_or_percent.process(linear[CONF_TO_X]),
await pixels_or_percent.process(linear[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(linear[CONF_EXTEND]),
)
elif direction == "RADIAL":
radial = gradient[CONF_RADIAL]
lv.grad_radial_init(
var,
await pixels_or_percent.process(radial[CONF_CENTER_X]),
await pixels_or_percent.process(radial[CONF_CENTER_Y]),
await pixels_or_percent.process(radial[CONF_TO_X]),
await pixels_or_percent.process(radial[CONF_TO_Y]),
await LV_GRAD_EXTEND.process(radial[CONF_EXTEND]),
)
if CONF_FOCAL_X in radial:
lv.grad_radial_set_focal(
var,
await pixels_or_percent.process(radial[CONF_FOCAL_X]),
await pixels_or_percent.process(radial[CONF_FOCAL_Y]),
radial.get(CONF_FOCAL_RADIUS, 0),
)
elif direction == "CONICAL":
conical = gradient[CONF_CONICAL]
lv.grad_conical_init(
var,
await pixels_or_percent.process(conical[CONF_CENTER_X]),
await pixels_or_percent.process(conical[CONF_CENTER_Y]),
await lv_angle_degrees.process(conical[CONF_START_ANGLE]),
await lv_angle_degrees.process(conical[CONF_END_ANGLE]),
await LV_GRAD_EXTEND.process(conical[CONF_EXTEND]),
)
stop_colors = cg.static_const_array(
ID(idbase + "_colors_", type=lv_color_t),
[await lv_color.process(x[CONF_COLOR]) for x in stops],
+2 -4
View File
@@ -23,10 +23,8 @@ void MAX31856Sensor::setup() {
void MAX31856Sensor::dump_config() {
LOG_SENSOR("", "MAX31856", this);
LOG_PIN(" CS Pin: ", this->cs_);
ESP_LOGCONFIG(
TAG, " Mains Filter: %s",
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown");
} else {
+6 -8
View File
@@ -80,14 +80,12 @@ void MAX31865Sensor::dump_config() {
LOG_SENSOR("", "MAX31865", this);
LOG_PIN(" CS Pin: ", this->cs_);
LOG_UPDATE_INTERVAL(this);
ESP_LOGCONFIG(
TAG,
" Reference Resistance: %.2fΩ\n"
" RTD: %u-wire %.2fΩ\n"
" Mains Filter: %s",
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
ESP_LOGCONFIG(TAG,
" Reference Resistance: %.2fΩ\n"
" RTD: %u-wire %.2fΩ\n"
" Mains Filter: %s",
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
}
void MAX31865Sensor::read_data_() {
+2 -3
View File
@@ -319,7 +319,7 @@ uint16_t Mcp4461Component::read_wiper_level_(uint8_t wiper_idx, bool *ok) {
if (!(this->read_16_(reg, &buf))) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper_idx);
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? "nonvolatile " : "", wiper_idx);
return 0;
}
if (ok != nullptr) {
@@ -377,8 +377,7 @@ void Mcp4461Component::write_wiper_level_(uint8_t wiper, uint16_t value) {
if (!(this->mcp4461_write_(this->get_wiper_address_(wiper), value, nonvolatile))) {
this->error_code_ = MCP4461_STATUS_I2C_ERROR;
this->status_set_warning();
ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper,
value);
ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? "nonvolatile " : "", wiper, value);
}
}
@@ -122,7 +122,7 @@ void MediaPlayerCall::perform() {
ESP_LOGV(TAG, " Volume: %.2f", this->volume_.value());
}
if (this->announcement_.has_value()) {
ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? LOG_STR_LITERAL("yes") : LOG_STR_LITERAL("no"));
ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? "yes" : "no");
}
this->parent_->control(*this);
}
+2 -7
View File
@@ -12,12 +12,7 @@ from esphome.components.const import (
CONF_DRAW_ROUNDING,
)
from esphome.components.display import CONF_SHOW_TEST_CARD
from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
only_on_variant,
)
from esphome.components.esp32 import VARIANT_ESP32P4, VARIANT_ESP32S3, only_on_variant
from esphome.components.mipi import (
COLOR_ORDERS,
CONF_DE_PIN,
@@ -231,7 +226,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
config = cv.All(
schema,
cv.only_on_esp32,
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]),
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
)(config)
model = MODELS[config[CONF_MODEL].upper()]
model.check_requirements()
+2 -3
View File
@@ -1,4 +1,4 @@
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "mipi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/hal.h"
@@ -400,5 +400,4 @@ void MipiRgb::dump_config() {
}
} // namespace esphome::mipi_rgb
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) ||
// defined(USE_ESP32_VARIANT_ESP32S31)
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
+1 -1
View File
@@ -1,6 +1,6 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/gpio.h"
#include "esphome/components/display/display.h"
#include "esp_lcd_panel_ops.h"
+1 -2
View File
@@ -25,8 +25,7 @@ void internal_dump_config(const char *model, int width, int height, int offset_w
" SPI Bus width: %d",
model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)),
YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(has_hardware_rotation), YESNO(invert_colors),
(madctl & MADCTL_BGR) ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"), display_bits,
is_big_endian ? LOG_STR_LITERAL("Big") : LOG_STR_LITERAL("Little"), spi_mode,
(madctl & MADCTL_BGR) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
static_cast<unsigned>(data_rate / 1000000), bus_width);
LOG_PIN(" CS Pin: ", cs);
LOG_PIN(" Reset Pin: ", reset);
+65 -121
View File
@@ -3,7 +3,6 @@
#include <algorithm>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -13,49 +12,29 @@ static const char *const TAG = "modbus";
static constexpr size_t MODBUS_MAX_LOG_BYTES = 64;
static constexpr uint32_t US_PER_SEC = 1000000;
static constexpr uint32_t US_PER_MS = 1000;
// Minimum interframe delay per the Modbus spec (fixed 1750us above 19200 baud)
static constexpr uint32_t MODBUS_MIN_FRAME_DELAY_US = 1750;
// Diagnostics only: the backdated byte stamp can precede last_send_ (echo, or noise during our own
// send), where an unsigned wrap would print ~4.29e9.
static uint32_t us_since_send(uint32_t last_modbus_byte, uint32_t last_send) {
const uint32_t elapsed = last_modbus_byte - last_send;
return (int32_t) elapsed < 0 ? 0 : elapsed;
}
// Approximate bits per character on the wire (depends on parity/stop bit config)
static constexpr uint32_t MODBUS_BITS_PER_CHAR = 11;
static constexpr uint32_t MS_PER_SEC = 1000;
void Modbus::setup() {
if (this->flow_control_pin_ != nullptr) {
this->flow_control_pin_->setup();
}
// RTU specifies 11 bits per character but 8N1 is 10, so derive it from the framing. The schema
// forbids a zero, so one here means the hub never set it (weikai): fall back to 8N1 and a 1 baud floor.
const uint8_t data_bits = this->parent_->get_data_bits() != 0 ? this->parent_->get_data_bits() : 8;
const uint8_t stop_bits = this->parent_->get_stop_bits() != 0 ? this->parent_->get_stop_bits() : 1;
const uint32_t baud_rate = std::max<uint32_t>(1u, this->parent_->get_baud_rate());
this->bits_per_char_ = static_cast<uint8_t>(
1 + data_bits + (this->parent_->get_parity() == uart::UART_CONFIG_PARITY_NONE ? 0 : 1) + stop_bits);
// 3.5 characters * bits per character * 1e6 us/sec / (bits/sec) (Standard modbus frame delay)
this->frame_delay_us_ =
std::max(MODBUS_MIN_FRAME_DELAY_US, (uint32_t) (3.5 * this->bits_per_char_ * US_PER_SEC / baud_rate) + 1);
this->frame_delay_ms_ =
std::max(2, // 1750us minimum per spec - rounded up to 2ms.
// 3.5 characters * 11 bits per character * 1000ms/sec / (bits/sec) (Standard modbus frame delay)
(uint16_t) (3.5 * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1);
// When rx_full_threshold is configured (non-zero), the UART has a hardware FIFO with a
// meaningful threshold (e.g., ESP32 native UART), so we can calculate a precise delay.
// Otherwise (e.g., USB UART), use 50ms to handle data arriving in chunks.
static constexpr uint32_t DEFAULT_LONG_RX_BUFFER_DELAY_US = 50 * US_PER_MS;
static constexpr uint16_t DEFAULT_LONG_RX_BUFFER_DELAY_MS = 50;
size_t rx_threshold = this->parent_->get_rx_full_threshold();
this->long_rx_buffer_delay_us_ = rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET
? (uint32_t) (rx_threshold * this->bits_per_char_ * US_PER_SEC / baud_rate) + 1
: DEFAULT_LONG_RX_BUFFER_DELAY_US;
// The idle-timeout interrupt fires rx_timeout characters after the last byte, so that much silence
// has already passed by the time we read it: backdate so the gap measures silence on the wire.
this->rx_detect_latency_us_ =
(uint32_t) (this->parent_->get_rx_timeout() * this->bits_per_char_ * US_PER_SEC / baud_rate);
this->long_rx_buffer_delay_ms_ =
rx_threshold != uart::UARTComponent::RX_FULL_THRESHOLD_UNSET
? (rx_threshold * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate()) + 1
: DEFAULT_LONG_RX_BUFFER_DELAY_MS;
}
void Modbus::loop() {
@@ -73,7 +52,7 @@ void ModbusClientHub::loop() {
// Send-wait watchdog: only the cheap time check runs at loop rate; expire_waiting_() looks the
// entry up and holds off if the response has started arriving.
if (this->waiting_for_response_ &&
this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_us_) {
this->last_receive_check_ - this->last_send_ > this->last_send_tx_offset_ + this->send_wait_time_) {
this->expire_waiting_();
}
@@ -93,7 +72,7 @@ void ModbusClientHub::expire_waiting_() {
}
// Only a genuine WAITING entry warrants the log (a cleared or interrupted shell timing out is expected).
if (cmd->state == FrameState::WAITING) {
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "us after last send", cmd->frame.address(),
ESP_LOGW(TAG, "Stop waiting for response from %" PRIu8 " %" PRIu32 "ms after last send", cmd->frame.address(),
this->last_receive_check_ - this->last_send_);
}
// Deliver on_no_response directly, the way the parse path delivers response()/error(): the entry
@@ -107,47 +86,37 @@ void ModbusClientHub::expire_waiting_() {
bool Modbus::timeout_() {
// If the response frame is finished (including interframe delay) - we timeout.
// The long_rx_buffer_delay accounts for long responses (larger than the UART rx_full_threshold) to avoid timeouts
// when the buffer is filling the back half of the response. The latch decides, not the current size:
// parsing a leading frame can shrink the buffer below the threshold while the rest is still streaming.
// The latency term covers the final batch, which is idle-delivered.
const uint32_t timeout =
this->exceeded_rx_full_threshold_
? std::max(this->frame_delay_us_, this->long_rx_buffer_delay_us_ + this->rx_detect_latency_us_)
: this->frame_delay_us_;
// when the buffer is filling the back half of the response
const uint16_t timeout = std::max(
(uint16_t) this->frame_delay_ms_,
(uint16_t) (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold() ? this->long_rx_buffer_delay_ms_
: 0));
return this->last_receive_check_ - this->last_modbus_byte_ > timeout;
}
// We use micros() here and elsewhere instead of App.get_loop_component_start_time() to avoid stale timestamps
// It's critical in all timestamp comparisons that the left timestamp comes before the right one in time
// If we use a cached value in place of micros() and last_modbus_byte_ is updated inside our loop
// then the comparison is backwards (small negative which wraps to large positive) and will cause a false timeout
// So in this component we don't use any cached timestamp values to avoid these annoying bugs.
// Compare before subtracting: a signed difference would read a bus idle past half the micros() wrap
// (~35 min) as a huge delay still owed.
static inline uint32_t remaining_delay(uint32_t elapsed, uint32_t required) {
return elapsed >= required ? 0 : required - elapsed;
}
int32_t Modbus::tx_delay_remaining() {
const uint32_t now = micros();
return (int32_t) std::max(remaining_delay(now - this->last_send_, this->last_send_tx_offset_ + this->frame_delay_us_),
remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_));
// millis() here and everywhere in this component, never a cached loop timestamp: a cached "now" can
// predate last_modbus_byte_, and the unsigned subtraction then wraps huge and forces a false timeout.
const uint32_t now = millis();
return std::max({(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ - (now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ - (now - this->last_modbus_byte_))});
}
int32_t ModbusClientHub::tx_delay_remaining() {
const uint32_t now = micros();
return (int32_t) std::max(
remaining_delay(now - this->last_send_,
this->last_send_tx_offset_ + this->frame_delay_us_ + this->turnaround_delay_us_),
remaining_delay(now - this->last_modbus_byte_, this->frame_delay_us_ + this->turnaround_delay_us_));
const uint32_t now = millis();
return std::max({(int32_t) 0,
(int32_t) (this->last_send_tx_offset_ + this->frame_delay_ms_ + this->turnaround_delay_ms_ -
(now - this->last_send_)),
(int32_t) (this->frame_delay_ms_ + this->turnaround_delay_ms_ - (now - this->last_modbus_byte_))});
}
bool Modbus::tx_blocked() {
// Blocked while any rx bytes are pending, or within tx_delay of the last byte in either direction
// (receivers must see our previous tx as done, and more rx may be coming). A remaining delay up to
// MODBUS_TX_MAX_DELAY_US doesn't block - send_frame_ absorbs it instead of looping on small waits.
return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_US;
// MODBUS_TX_MAX_DELAY_MS doesn't block - send_frame_ absorbs it instead of looping on small waits.
return this->available() || !this->rx_buffer_.empty() || this->tx_delay_remaining() > MODBUS_TX_MAX_DELAY_MS;
}
bool ModbusClientHub::tx_blocked() { return this->waiting_for_response_ || this->Modbus::tx_blocked(); }
@@ -164,26 +133,20 @@ bool ModbusClientHub::tx_buffer_empty() {
}
void Modbus::receive_bytes_() {
this->last_receive_check_ = micros();
this->last_receive_check_ = millis();
size_t bytes = this->available();
if (bytes) {
size_t buffer_size = this->rx_buffer_.size();
// Below the threshold the batch can only be idle-delivered, so its last byte finished one detection
// latency ago; at or above it the frame may still be streaming, so stamp now.
this->last_modbus_byte_ = bytes < this->parent_->get_rx_full_threshold()
? this->last_receive_check_ - this->rx_detect_latency_us_
: this->last_receive_check_;
this->last_modbus_byte_ = this->last_receive_check_;
this->rx_buffer_.resize(buffer_size + bytes);
if (!this->read_array(this->rx_buffer_.data() + buffer_size, bytes)) {
this->rx_buffer_.resize(buffer_size);
return;
}
if (this->rx_buffer_.size() >= this->parent_->get_rx_full_threshold())
this->exceeded_rx_full_threshold_ = true;
if (buffer_size == 0) {
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "us after last send",
this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), micros() - this->last_send_);
ESP_LOGV(TAG, "Received first byte %" PRIu8 " (0X%x) of %zu bytes %" PRIu32 "ms after last send",
this->rx_buffer_[0], this->rx_buffer_[0], this->rx_buffer_.size(), millis() - this->last_send_);
}
}
}
@@ -336,8 +299,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
ModbusDeviceCommand *cmd = this->waiting_for_response_ ? this->find_waiting_() : nullptr;
if (cmd == nullptr) {
ESP_LOGW(TAG,
"Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "us after last send",
address, function_code, us_since_send(this->last_modbus_byte_, this->last_send_));
"Received unexpected frame from address %" PRIu8 ", function code 0x%X, %" PRIu32 "ms after last send",
address, function_code, this->last_modbus_byte_ - this->last_send_);
return;
}
@@ -347,9 +310,9 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
if (expected_address != address || expected_function_code != (function_code & FUNCTION_CODE_MASK)) {
ESP_LOGW(TAG,
"Received incorrect frame address %" PRIu8 " <> %" PRIu8 " or function code 0x%X <> 0x%X, %" PRIu32
"us after last send",
"ms after last send",
address, expected_address, (function_code & FUNCTION_CODE_MASK), expected_function_code,
us_since_send(this->last_modbus_byte_, this->last_send_));
this->last_modbus_byte_ - this->last_send_);
// Unexpected frame: flip a WAITING entry to an INTERRUPTED shell that ignores the rest of this
// transaction and blocks tx until the send-wait timeout, where it gets its on_no_response.
cmd->interrupt();
@@ -362,8 +325,8 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
// cleared-interrupted frame still ends in on_no_response rather than delivering a late response.
ESP_LOGW(TAG,
"Ignoring response from %" PRIu8 " - transmission interrupted by previous unexpected response, %" PRIu32
"us after last send",
address, us_since_send(this->last_modbus_byte_, this->last_send_));
"ms after last send",
address, this->last_modbus_byte_ - this->last_send_);
return;
}
@@ -374,12 +337,12 @@ void ModbusClientHub::process_modbus_server_frame(uint8_t address, std::span<con
this->sweep_needed_ = true;
if (helpers::is_function_code_exception(function_code)) {
uint8_t exception = pdu[1]; // exception frames are fixed-length, so the code is always present
ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "us after last send",
function_code, exception, address, us_since_send(this->last_modbus_byte_, this->last_send_));
ESP_LOGW(TAG, "Error function code: 0x%X exception: %" PRIu8 ", address: %" PRIu8 ", %" PRIu32 "ms after last send",
function_code, exception, address, this->last_modbus_byte_ - this->last_send_);
cmd->error(static_cast<ExceptionCode>(exception));
} else if (!cmd->response(pdu)) {
ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "us after last send", address,
us_since_send(this->last_modbus_byte_, this->last_send_));
ESP_LOGV(TAG, "Ignoring response from %" PRIu8 " - no callback device set, %" PRIu32 "ms after last send", address,
this->last_modbus_byte_ - this->last_send_);
}
}
@@ -775,16 +738,9 @@ void ModbusServerHub::process_modbus_client_frame_(uint8_t address, uint8_t func
// Callers gate on tx_blocked() first, but the pre-send delay below can span several ms, so re-check
// after it and refuse (return false) if a byte arrived in that window rather than transmit over it.
bool Modbus::send_frame_(const ModbusFrame &frame) {
int32_t tx_delay_remaining = this->tx_delay_remaining();
const int32_t tx_delay_remaining = this->tx_delay_remaining();
if (tx_delay_remaining > 0) {
// Yield the whole-ms part: delay() never blocks past the request on FreeRTOS, and only slightly
// over elsewhere, which just lengthens the gap. The recompute below makes the remainder exact.
if (tx_delay_remaining >= (int32_t) US_PER_MS) {
delay(tx_delay_remaining / US_PER_MS);
tx_delay_remaining = this->tx_delay_remaining();
}
if (tx_delay_remaining > 0)
delayMicroseconds(tx_delay_remaining);
delay(tx_delay_remaining);
}
if (this->tx_blocked()) {
@@ -799,15 +755,14 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
this->last_send_tx_offset_ = 0;
} else {
this->write_array(frame.data.data(), frame.size());
this->last_send_tx_offset_ =
frame.size() * this->bits_per_char_ * US_PER_SEC / std::max<uint32_t>(1u, this->parent_->get_baud_rate()) + 1;
this->last_send_tx_offset_ = frame.size() * MODBUS_BITS_PER_CHAR * MS_PER_SEC / this->parent_->get_baud_rate() + 1;
}
uint32_t now = micros();
uint32_t now = millis();
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char hex_buf[format_hex_pretty_size(MODBUS_MAX_LOG_BYTES)];
#endif
ESP_LOGV(TAG, "Write: %s %" PRIu32 "us after last send, %" PRIu32 "us after last receive",
ESP_LOGV(TAG, "Write: %s %" PRIu32 "ms after last send, %" PRIu32 "ms after last receive",
format_hex_pretty_to(hex_buf, frame.data.data(), frame.size()), now - this->last_send_,
now - this->last_modbus_byte_);
this->last_send_ = now;
@@ -815,9 +770,6 @@ bool Modbus::send_frame_(const ModbusFrame &frame) {
}
void ModbusClientHub::send_next_frame_() {
if (this->tx_buffer_.empty())
return;
if (this->tx_blocked())
return;
@@ -848,25 +800,20 @@ void ModbusClientHub::send_next_frame_() {
void ModbusClientHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Modbus:\n"
" Send Wait Time: %" PRIu32 " ms\n"
" Turnaround Time: %" PRIu32 " ms\n"
" Frame Delay: %" PRIu32 " us\n"
" Long Rx Buffer Delay: %" PRIu32 " us\n"
" Bits Per Character: %" PRIu8 "\n"
" Rx Detect Latency: %" PRIu32 " us",
this->send_wait_time_us_ / US_PER_MS, this->turnaround_delay_us_ / US_PER_MS, this->frame_delay_us_,
this->long_rx_buffer_delay_us_, this->bits_per_char_, this->rx_detect_latency_us_);
" Send Wait Time: %" PRIu16 " ms\n"
" Turnaround Time: %" PRIu16 " ms\n"
" Frame Delay: %" PRIu16 " ms\n"
" Long Rx Buffer Delay: %" PRIu16 " ms",
this->send_wait_time_, this->turnaround_delay_ms_, this->frame_delay_ms_,
this->long_rx_buffer_delay_ms_);
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
}
void ModbusServerHub::dump_config() {
ESP_LOGCONFIG(TAG,
"Modbus:\n"
" Frame Delay: %" PRIu32 " us\n"
" Long Rx Buffer Delay: %" PRIu32 " us\n"
" Bits Per Character: %" PRIu8 "\n"
" Rx Detect Latency: %" PRIu32 " us",
this->frame_delay_us_, this->long_rx_buffer_delay_us_, this->bits_per_char_,
this->rx_detect_latency_us_);
" Frame Delay: %" PRIu16 " ms\n"
" Long Rx Buffer Delay: %" PRIu16 " ms",
this->frame_delay_ms_, this->long_rx_buffer_delay_ms_);
LOG_PIN(" Flow Control Pin: ", this->flow_control_pin_);
}
@@ -1195,8 +1142,7 @@ void ModbusServerHub::send_raw_(const uint8_t *payload, uint16_t len) {
// without a heap allocation. Only one server reply is ever waiting, so a single buffer suffices.
std::memcpy(this->deferred_payload_.data(), payload, len);
this->deferred_payload_len_ = len;
// set_timeout() takes milliseconds; round the microsecond delay up so we never fire early.
this->set_timeout("deferred_send", (this->tx_delay_remaining() + US_PER_MS - 1) / US_PER_MS, [this]() {
this->set_timeout("deferred_send", this->tx_delay_remaining(), [this]() {
ModbusFrame frame(this->deferred_payload_[0], this->deferred_payload_.data() + 1,
this->deferred_payload_len_ - 1);
if (!this->send_frame_(frame))
@@ -1216,11 +1162,11 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
bytes = bytes_to_clear;
if (bytes > 0) {
if (warn) {
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
ESP_LOGW(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
millis() - this->last_send_);
} else {
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "us after last send", bytes, LOG_STR_ARG(reason),
micros() - this->last_send_);
ESP_LOGV(TAG, "Clearing buffer of %zu bytes - %s %" PRIu32 "ms after last send", bytes, LOG_STR_ARG(reason),
millis() - this->last_send_);
}
if (bytes == this->rx_buffer_.size()) {
this->rx_buffer_.clear();
@@ -1228,8 +1174,6 @@ void Modbus::clear_rx_buffer_(const LogString *reason, bool warn, size_t bytes_t
this->rx_buffer_.erase(this->rx_buffer_.begin(), this->rx_buffer_.begin() + bytes);
}
}
if (this->rx_buffer_.empty())
this->exceeded_rx_full_threshold_ = false;
}
void ModbusClientDevice::dispatch_response_(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu,
+7 -14
View File
@@ -19,7 +19,7 @@ namespace esphome::modbus {
// Tx queue backstop: duplicates dedup into one entry, so only a runaway generator of distinct frames
// (e.g. a loop writing a changing value) could grow the heap unboundedly.
static constexpr uint16_t MODBUS_TX_BUFFER_SIZE = 128;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_US = 5000;
static constexpr uint16_t MODBUS_TX_MAX_DELAY_MS = 5;
// Typical frames -- reads and single-register/coil writes -- are exactly 8 bytes
// (address + 5-byte PDU + 2-byte CRC).
@@ -70,18 +70,12 @@ class Modbus : public uart::UARTDevice, public Component {
bool send_frame_(const ModbusFrame &frame);
uint16_t find_frame_end_by_crc_(uint16_t min_length) const;
// All timestamps and durations below are micros()-based
uint32_t last_modbus_byte_{0};
uint32_t last_receive_check_{0};
uint32_t last_send_{0};
uint32_t last_send_tx_offset_{0};
uint32_t frame_delay_us_{5000};
uint32_t long_rx_buffer_delay_us_{0};
uint32_t rx_detect_latency_us_{0};
// Bits on the wire per character (start + data + optional parity + stop); 12 at most.
uint8_t bits_per_char_{11};
// Latched when a read reaches rx_full_threshold, cleared when the buffer drains.
bool exceeded_rx_full_threshold_{false};
uint16_t frame_delay_ms_{5};
uint16_t long_rx_buffer_delay_ms_{0};
GPIOPin *flow_control_pin_{nullptr};
@@ -238,9 +232,8 @@ class ModbusClientHub : public Modbus {
ModbusClientHub() = default;
void dump_config() override;
void loop() override;
// Config arrives in milliseconds; stored internally in microseconds like all other timing.
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_us_ = time_in_ms * 1000UL; }
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_us_ = time_in_ms * 1000UL; }
void set_send_wait_time(uint16_t time_in_ms) { this->send_wait_time_ = time_in_ms; }
void set_turnaround_time(uint16_t time_in_ms) { this->turnaround_delay_ms_ = time_in_ms; }
bool tx_buffer_empty();
bool tx_blocked() override;
ESPDEPRECATED("Use queue_pdu() with create_client_pdu() instead. Removed in 2026.10.0", "2026.4.0")
@@ -286,8 +279,8 @@ class ModbusClientHub : public Modbus {
// End the wait for a response on send-wait timeout (the loop() watchdog body); see FrameState.
void expire_waiting_();
uint32_t send_wait_time_us_{2000000};
uint32_t turnaround_delay_us_{0};
uint16_t send_wait_time_{2000};
uint16_t turnaround_delay_ms_{0};
// Set on transmit, cleared on the transaction-ending transition; send_next_frame_ won't select
// while it is set, so at most one frame is awaiting a response.
@@ -269,8 +269,7 @@ void ModbusServer::dump_config() {
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %" PRIu16,
this->address_,
this->server_courtesy_response_.enabled ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
@@ -281,9 +280,8 @@ void ModbusServer::dump_config() {
}
ESP_LOGCONFIG(TAG, "server bits");
for (auto &b : this->server_bits_) {
ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address,
b->read_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
b->write_lambda ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
ESP_LOGCONFIG(TAG, " Address=0x%04X readable=%s writable=%s", b->address, b->read_lambda ? "true" : "false",
b->write_lambda ? "true" : "false");
}
#endif
}
+3 -2
View File
@@ -7,6 +7,7 @@ from esphome.components.esp32 import (
add_idf_sdkconfig_option,
idf_version,
include_builtin_idf_component,
request_tls,
)
from esphome.config_helpers import (
filter_source_files_from_defines,
@@ -364,8 +365,8 @@ async def to_code(config):
add_idf_component(name="espressif/mqtt", ref="1.0.0")
else:
include_builtin_idf_component("mqtt")
# mqtt_client.h drags in esp_tls types; esp-tls is excluded by default
include_builtin_idf_component("esp-tls")
# esp-mqtt links transport_ssl.c (esp_tls) even for plain MQTT
request_tls()
cg.add_define("USE_MQTT")
cg.add_global(mqtt_ns.using)
+1 -4
View File
@@ -289,10 +289,7 @@ async def to_code(config):
)
if CORE.is_esp32:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
# and esp-tls, whose sdkconfig options below need the component present
esp32.include_builtin_idf_component("esp_http_client")
esp32.include_builtin_idf_component("esp-tls")
esp32.request_http_client()
esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True
+2 -3
View File
@@ -595,8 +595,7 @@ void Nextion::process_nextion_commands_() {
uint8_t page_id = to_process[0];
uint8_t component_id = to_process[1];
uint8_t touch_event = to_process[2]; // 0 -> release, 1 -> press
ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"),
page_id, component_id);
ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? "PRESS" : "RELEASE", page_id, component_id);
for (auto *touch : this->touch_) {
touch->process_touch(page_id, component_id, touch_event != 0);
}
@@ -629,7 +628,7 @@ void Nextion::process_nextion_commands_() {
const uint16_t x = (uint16_t(to_process[0]) << 8) | to_process[1];
const uint16_t y = (uint16_t(to_process[2]) << 8) | to_process[3];
const uint8_t touch_event = to_process[4]; // 0 -> release, 1 -> press
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"), x, y);
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? "PRESS" : "RELEASE", x, y);
break;
}
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
only_on_variant,
require_mbedtls_ecp,
require_vfs_select,
)
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
@@ -282,6 +283,8 @@ async def to_code(config: ConfigType) -> None:
# Re-enable openthread IDF component (excluded by default)
if CORE.is_esp32:
include_builtin_idf_component("openthread")
# OPENTHREAD_CONFIG_ECDSA_ENABLE: the SRP client host key uses mbedtls_ecdsa_*
require_mbedtls_ecp()
cg.add_define("USE_OPENTHREAD")
if config.get(CONF_FORCE_DATASET):
+1 -2
View File
@@ -119,8 +119,7 @@ void PCM5122::dump_config() {
" Channel mix: %s\n"
" Volume range: %.1f dB to %.1f dB\n"
" Muted: %s",
this->bits_per_sample_,
this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? LOG_STR_LITERAL("0 dB") : LOG_STR_LITERAL("-6 dB"),
this->bits_per_sample_, this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? "0 dB" : "-6 dB",
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
LOG_PIN(" Enable Pin: ", this->enable_pin_);
}
+2 -2
View File
@@ -243,8 +243,8 @@ uint8_t PN7150::reset_core_(const bool reset_config, const bool power) {
}
ESP_LOGD(TAG, "Configuration %s, NCI version: %s",
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"),
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0"));
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 2] ? "reset" : "retained",
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0");
return nfc::STATUS_OK;
}
+2 -2
View File
@@ -265,8 +265,8 @@ uint8_t PN7160::reset_core_(const bool reset_config, const bool power) {
}
ESP_LOGD(TAG, "Configuration %s, NCI version: %s, Manufacturer ID: 0x%02X",
rx.get_message()[4] ? LOG_STR_LITERAL("reset") : LOG_STR_LITERAL("retained"),
rx.get_message()[5] == 0x20 ? LOG_STR_LITERAL("2.0") : LOG_STR_LITERAL("1.0"), rx.get_message()[6]);
rx.get_message()[4] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0",
rx.get_message()[6]);
rx.get_message().erase(rx.get_message().begin(), rx.get_message().begin() + 8);
char mfr_buf[nfc::FORMAT_BYTES_BUFFER_SIZE];
ESP_LOGD(TAG, "Manufacturer info: %s", nfc::format_bytes_to(mfr_buf, rx.get_message()));
+1 -2
View File
@@ -137,8 +137,7 @@ void PylontechComponent::process_line_(std::string &buffer) {
} else if (strcmp(token_buf, "Power") == 0) {
// header line i.e. "Power Volt Curr" and so on
this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos;
ESP_LOGD(TAG, "header line %s Tlow.Id: %s",
this->has_tlow_id_ ? LOG_STR_LITERAL("with") : LOG_STR_LITERAL("without"),
ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without",
buffer.substr(0, buffer.size() - 2).c_str());
return;
} else {
+2 -3
View File
@@ -55,9 +55,8 @@ void RD03DComponent::setup() {
void RD03DComponent::dump_config() {
ESP_LOGCONFIG(TAG, "RD-03D:");
if (this->tracking_mode_.has_value()) {
ESP_LOGCONFIG(
TAG, " Tracking Mode: %s",
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? LOG_STR_LITERAL("single") : LOG_STR_LITERAL("multi"));
ESP_LOGCONFIG(TAG, " Tracking Mode: %s",
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? "single" : "multi");
}
if (this->throttle_ > 0) {
ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_);
@@ -76,16 +76,15 @@ void RemoteReceiverComponent::setup() {
}
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(
TAG,
"Remote Receiver:\n"
" Buffer Size: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
this->filter_us_, this->idle_us_);
ESP_LOGCONFIG(TAG,
"Remote Receiver:\n"
" Buffer Size: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
this->idle_us_);
LOG_PIN(" Pin: ", this->pin_);
}
@@ -11,8 +11,8 @@ void ResistanceSensor::dump_config() {
" Configuration: %s\n"
" Resistor: %.2fΩ\n"
" Reference Voltage: %.1fV",
this->configuration_ == UPSTREAM ? LOG_STR_LITERAL("UPSTREAM") : LOG_STR_LITERAL("DOWNSTREAM"),
this->resistor_, this->reference_voltage_);
this->configuration_ == UPSTREAM ? "UPSTREAM" : "DOWNSTREAM", this->resistor_,
this->reference_voltage_);
}
void ResistanceSensor::process_(float value) {
if (std::isnan(value)) {
+1 -1
View File
@@ -13,7 +13,7 @@ void Sen21231Sensor::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? LOG_STR_LITERAL("FAILED") : LOG_STR_LITERAL("OK"));
ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? "FAILED" : "OK");
LOG_UPDATE_INTERVAL(this);
}
+10 -69
View File
@@ -9,11 +9,9 @@ static const char *const TAG = "sen6x";
static constexpr uint8_t POLL_RETRIES = 24; // 24 attempts
static constexpr uint32_t I2C_READ_DELAY = 20; // 20 ms to wait for I2C read to complete
static constexpr uint32_t CMD_EXEC_DELAY = 20; // execution time of set commands (datasheet section 4.8)
static constexpr uint32_t POLL_INTERVAL = 50; // 50 ms between poll attempts
// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID is active at a time.
// Single numeric timeout ID — the chain is sequential so only one is active at a time.
static constexpr uint32_t TIMEOUT_POLL = 1;
static constexpr uint32_t TIMEOUT_SETUP_STEP = 2;
static constexpr uint16_t SEN6X_CMD_GET_DATA_READY_STATUS = 0x0202;
static constexpr uint16_t SEN6X_CMD_GET_FIRMWARE_VERSION = 0xD100;
static constexpr uint16_t SEN6X_CMD_GET_PRODUCT_NAME = 0xD014;
@@ -28,8 +26,6 @@ static constexpr uint16_t SEN6X_CMD_READ_MEASUREMENT_SEN69C = 0x04B5;
static constexpr uint16_t SEN6X_CMD_START_MEASUREMENTS = 0x0021;
static constexpr uint16_t SEN6X_CMD_RESET = 0xD304;
static constexpr uint16_t SEN6X_CMD_VOC_ALGORITHM_TUNING = 0x60D0;
static constexpr uint16_t SEN6X_CMD_NOX_ALGORITHM_TUNING = 0x60E1;
static inline void set_read_command_and_words(SEN6XComponent::Sen6xType type, uint16_t &read_cmd, uint8_t &read_words) {
read_cmd = SEN6X_CMD_READ_MEASUREMENT;
@@ -147,76 +143,21 @@ void SEN6XComponent::setup() {
this->firmware_version_minor_ = raw_firmware_version & 0xFF;
ESP_LOGI(TAG, "Firmware: %u.%u", this->firmware_version_major_, this->firmware_version_minor_);
// Step 4: write configuration commands one at a time, then start measurements.
// Delay the first step so it doesn't run in the same loop tick as the read above.
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
});
});
});
});
}
// One configuration write per invocation, spaced by CMD_EXEC_DELAY. Cases without a
// configured value fall through; each taken case must advance setup_step_index_ so the
// next invocation resumes at the following step. These writes are optional, so a failure
// only warns and the chain continues to the mandatory start-measurements write.
void SEN6XComponent::run_next_setup_step_() {
switch (this->setup_step_index_) {
// Tuning writes are skipped when setup() disabled the sensor for this variant
case 0:
this->setup_step_index_++;
if (this->voc_sensor_ != nullptr && this->voc_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN6X_CMD_VOC_ALGORITHM_TUNING, this->voc_tuning_params_.value());
break;
}
[[fallthrough]];
case 1:
this->setup_step_index_++;
if (this->nox_sensor_ != nullptr && this->nox_tuning_params_.has_value()) {
this->write_tuning_parameters_(SEN6X_CMD_NOX_ALGORITHM_TUNING, this->nox_tuning_params_.value());
break;
}
[[fallthrough]];
default:
this->finish_setup_();
return;
}
this->set_timeout(TIMEOUT_SETUP_STEP, CMD_EXEC_DELAY, [this]() { this->run_next_setup_step_(); });
}
void SEN6XComponent::finish_setup_() {
if (!this->write_command(SEN6X_CMD_START_MEASUREMENTS)) {
ESP_LOGE(TAG, "Write 0x%04X failed, error %d", SEN6X_CMD_START_MEASUREMENTS, this->last_error_);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
this->set_timeout(60000, [this]() { this->startup_complete_ = true; });
this->initialized_ = true;
ESP_LOGD(TAG, "Initialized");
}
// Writes one optional configuration command. A failure warns and returns false, but does
// not stop setup: the sensor still measures with that setting left at its default.
bool SEN6XComponent::write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len) {
if (!this->write_command(i2c_command, data, len)) {
ESP_LOGE(TAG, "Write 0x%04X failed, error %d", i2c_command, this->last_error_);
this->status_set_warning();
return false;
}
return true;
}
bool SEN6XComponent::write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning) {
uint16_t params[6] = {tuning.index_offset,
tuning.learning_time_offset_hours,
tuning.learning_time_gain_hours,
tuning.gating_max_duration_minutes,
tuning.std_initial,
tuning.gain_factor};
return this->write_config_words_(i2c_command, params, 6);
}
void SEN6XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"sen6x:\n"
+2 -40
View File
@@ -1,25 +1,11 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/optional.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/sensirion_common/i2c_sensirion.h"
namespace esphome::sen6x {
// The NOx algorithm requires std_initial to stay at 50 (Sensirion datasheet)
static constexpr uint16_t NOX_STD_INITIAL = 50;
// Raw parameter block for the VOC/NOx algorithm tuning commands
struct GasTuning {
uint16_t index_offset;
uint16_t learning_time_offset_hours;
uint16_t learning_time_gain_hours;
uint16_t gating_max_duration_minutes;
uint16_t std_initial;
uint16_t gain_factor;
};
class SEN6XComponent final : public PollingComponent, public sensirion_common::SensirionI2CDevice {
SUB_SENSOR(pm_1_0)
SUB_SENSOR(pm_2_5)
@@ -41,46 +27,22 @@ class SEN6XComponent final : public PollingComponent, public sensirion_common::S
enum Sen6xType { SEN62, SEN63C, SEN65, SEN66, SEN68, SEN69C, UNKNOWN };
void set_type(const std::string &type) { sen6x_type_ = infer_type_from_product_name_(type); }
void set_voc_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t std_initial, uint16_t gain_factor) {
this->voc_tuning_params_ = GasTuning{
index_offset, learning_time_offset_hours, learning_time_gain_hours, gating_max_duration_minutes, std_initial,
gain_factor};
}
void set_nox_algorithm_tuning(uint16_t index_offset, uint16_t learning_time_offset_hours,
uint16_t learning_time_gain_hours, uint16_t gating_max_duration_minutes,
uint16_t gain_factor) {
this->nox_tuning_params_ = GasTuning{index_offset,
learning_time_offset_hours,
learning_time_gain_hours,
gating_max_duration_minutes,
NOX_STD_INITIAL,
gain_factor};
}
protected:
Sen6xType infer_type_from_product_name_(const std::string &product_name);
void run_next_setup_step_();
void finish_setup_();
bool write_config_words_(uint16_t i2c_command, const uint16_t *data, uint8_t len);
bool write_tuning_parameters_(uint16_t i2c_command, const GasTuning &tuning);
void poll_data_ready_();
void read_measurements_();
void parse_and_publish_measurements_();
bool initialized_{false};
std::string product_name_;
std::string serial_number_;
optional<GasTuning> voc_tuning_params_;
optional<GasTuning> nox_tuning_params_;
Sen6xType sen6x_type_{UNKNOWN};
std::string serial_number_;
uint16_t read_cmd_{0};
uint8_t setup_step_index_{0};
uint8_t firmware_version_major_{0};
uint8_t firmware_version_minor_{0};
uint8_t poll_retries_remaining_{0};
uint8_t read_words_{0};
bool initialized_{false};
bool startup_complete_{false};
};
+8 -68
View File
@@ -3,22 +3,15 @@ from esphome.components import i2c, sensirion_common, sensor
from esphome.components.const import CONF_NOX_INDEX, CONF_VOC_INDEX
import esphome.config_validation as cv
from esphome.const import (
CONF_ALGORITHM_TUNING,
CONF_CO2,
CONF_FORMALDEHYDE,
CONF_GAIN_FACTOR,
CONF_GATING_MAX_DURATION_MINUTES,
CONF_HUMIDITY,
CONF_ID,
CONF_INDEX_OFFSET,
CONF_LEARNING_TIME_GAIN_HOURS,
CONF_LEARNING_TIME_OFFSET_HOURS,
CONF_NOX,
CONF_PM_1_0,
CONF_PM_2_5,
CONF_PM_4_0,
CONF_PM_10_0,
CONF_STD_INITIAL,
CONF_TEMPERATURE,
CONF_TYPE,
CONF_VOC,
@@ -51,42 +44,6 @@ SEN6XComponent = sen6x_ns.class_(
)
def _gas_index_schema(
*,
index_offset: int,
gating_max_duration: int,
std_initial: int | None,
) -> cv.Schema:
"""Sensor schema for a gas index sensor with optional algorithm tuning.
std_initial is only configurable for VOC; the NOx algorithm requires 50.
"""
tuning_schema = {
cv.Optional(CONF_INDEX_OFFSET, default=index_offset): cv.int_range(
min=1, max=250
),
cv.Optional(CONF_LEARNING_TIME_OFFSET_HOURS, default=12): cv.int_range(
min=1, max=1000
),
cv.Optional(CONF_LEARNING_TIME_GAIN_HOURS, default=12): cv.int_range(
min=1, max=1000
),
cv.Optional(
CONF_GATING_MAX_DURATION_MINUTES, default=gating_max_duration
): cv.int_range(min=0, max=3000),
cv.Optional(CONF_GAIN_FACTOR, default=230): cv.int_range(min=1, max=1000),
}
if std_initial is not None:
tuning_schema[cv.Optional(CONF_STD_INITIAL, default=std_initial)] = (
cv.int_range(min=10, max=5000)
)
return sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
).extend({cv.Optional(CONF_ALGORITHM_TUNING): cv.Schema(tuning_schema)})
CONFIG_SCHEMA = cv.All(
cv.rename_key(CONF_VOC, CONF_VOC_INDEX, removed_in="2027.2.0", component="sen6x"),
cv.rename_key(CONF_NOX, CONF_NOX_INDEX, removed_in="2027.2.0", component="sen6x"),
@@ -137,15 +94,15 @@ CONFIG_SCHEMA = cv.All(
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VOC_INDEX): _gas_index_schema(
index_offset=100,
gating_max_duration=180,
std_initial=50,
cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_NOX_INDEX): _gas_index_schema(
index_offset=1,
gating_max_duration=720,
std_initial=None,
cv.Optional(CONF_NOX_INDEX): sensor.sensor_schema(
icon=ICON_RADIATOR,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
@@ -192,20 +149,3 @@ async def to_code(config: ConfigType) -> None:
if cfg := config.get(key):
sens = await sensor.new_sensor(cfg)
cg.add(getattr(var, func_name)(sens))
for key, setter in (
(CONF_VOC_INDEX, "set_voc_algorithm_tuning"),
(CONF_NOX_INDEX, "set_nox_algorithm_tuning"),
):
if (tuning := config.get(key, {}).get(CONF_ALGORITHM_TUNING)) is not None:
args = [
tuning[CONF_INDEX_OFFSET],
tuning[CONF_LEARNING_TIME_OFFSET_HOURS],
tuning[CONF_LEARNING_TIME_GAIN_HOURS],
tuning[CONF_GATING_MAX_DURATION_MINUTES],
]
# std_initial is in the schema for VOC only
if (std_initial := tuning.get(CONF_STD_INITIAL)) is not None:
args.append(std_initial)
args.append(tuning[CONF_GAIN_FACTOR])
cg.add(getattr(var, setter)(*args))
+2 -3
View File
@@ -89,9 +89,8 @@ void SenseAirComponent::background_calibration_result() {
}
// Check if 5th bit (register CI6) is set
ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)",
(response[4] & 0b100000) != 0 ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("NOT_OK"), response[0], response[1],
response[2], response[3], response[4], response[5], response[6]);
ESP_LOGI(TAG, "SenseAir Result=%s (%02x%02x%02x %02x%02x %02x%02x)", (response[4] & 0b100000) != 0 ? "OK" : "NOT_OK",
response[0], response[1], response[2], response[3], response[4], response[5], response[6]);
}
void SenseAirComponent::abc_enable() {
@@ -82,11 +82,11 @@ void SerialProxy::dump_config() {
" RTS Pin: %s\n"
" DTR Pin: %s",
this->instance_index_, this->name_ != nullptr ? this->name_ : "",
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? LOG_STR_LITERAL("RS485")
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? LOG_STR_LITERAL("RS232")
: LOG_STR_LITERAL("TTL"),
this->rts_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"),
this->dtr_pin_ != nullptr ? LOG_STR_LITERAL("configured") : LOG_STR_LITERAL("not configured"));
this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS485 ? "RS485"
: this->port_type_ == api::enums::SERIAL_PROXY_PORT_TYPE_RS232 ? "RS232"
: "TTL",
this->rts_pin_ != nullptr ? "configured" : "not configured",
this->dtr_pin_ != nullptr ? "configured" : "not configured");
}
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
+1 -1
View File
@@ -284,7 +284,7 @@ void SGP4xComponent::dump_config() {
" Type: %s\n"
" Serial number: %" PRIu64 "\n"
" Minimum Samples: %f",
this->sgp_type_ == SGP41 ? LOG_STR_LITERAL("SGP41") : LOG_STR_LITERAL("SGP40"), this->serial_number_,
this->sgp_type_ == SGP41 ? "SGP41" : "SGP40", this->serial_number_,
GasIndexAlgorithm_INITIAL_BLACKOUT);
}
LOG_UPDATE_INTERVAL(this);
-2
View File
@@ -17,8 +17,6 @@ CONF_IMPLEMENTATION = "implementation"
IMPLEMENTATION_LWIP_TCP = "lwip_tcp"
IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets"
IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets"
# Implementations whose sockets cannot make outgoing connections
IMPLEMENTATIONS_WITHOUT_CONNECT = frozenset({IMPLEMENTATION_LWIP_TCP})
# Socket tracking infrastructure
# Components register their socket needs and platforms read this to configure appropriately
@@ -59,15 +59,13 @@ int BSDSocketImpl::close() {
int BSDSocketImpl::setblocking(bool blocking) {
int fl = ::fcntl(this->fd_, F_GETFL, 0);
if (fl < 0) {
return fl;
}
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
return ::fcntl(this->fd_, F_SETFL, fl);
::fcntl(this->fd_, F_SETFL, fl);
return 0;
}
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
@@ -49,15 +49,13 @@ int LwIPSocketImpl::close() {
int LwIPSocketImpl::setblocking(bool blocking) {
int fl = lwip_fcntl(this->fd_, F_GETFL, 0);
if (fl < 0) {
return fl;
}
if (blocking) {
fl &= ~O_NONBLOCK;
} else {
fl |= O_NONBLOCK;
}
return lwip_fcntl(this->fd_, F_SETFL, fl);
lwip_fcntl(this->fd_, F_SETFL, fl);
return 0;
}
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
+2 -54
View File
@@ -2,9 +2,6 @@
#if defined(USE_SOCKET_IMPL_LWIP_TCP) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS)
#include <cerrno>
#include <cstring>
#ifdef USE_SOCKET_IMPL_BSD_SOCKETS
#include <sys/select.h>
#endif
#include <string>
#include "esphome/core/log.h"
#include "esphome/core/application.h"
@@ -168,10 +165,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
#else
// Use LWIP-specific functions
ip6_addr_t ip6;
if (inet6_aton(ip_address, &ip6) == 0) {
errno = EINVAL;
return 0;
}
inet6_aton(ip_address, &ip6);
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
#endif
return sizeof(sockaddr_in6);
@@ -191,58 +185,12 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
return 0;
}
#else
// Unlike inet_addr(), inet_aton() can signal failure while still
// accepting the broadcast address 255.255.255.255
if (inet_aton(ip_address, &server->sin_addr) == 0) {
errno = EINVAL;
return 0;
}
server->sin_addr.s_addr = inet_addr(ip_address);
#endif
server->sin_port = htons(port);
return sizeof(sockaddr_in);
}
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
ConnectPollResult poll_connect(Socket &sock, int &err_out) {
int fd = sock.get_fd();
if (fd < 0 || fd >= FD_SETSIZE) {
// FD_SET on either is undefined behavior
err_out = EBADF;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
// Connect completion is a write event; the main loop only selects on reads
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
#ifdef USE_SOCKET_IMPL_LWIP_SOCKETS
// LWIP_COMPAT_SOCKETS may be off (LibreTiny), so use the lwip symbol directly
int ret = lwip_select(fd + 1, nullptr, &writefds, nullptr, &tv);
#else
// Global-scope select: the entity namespace esphome::select shadows it here
int ret = ::select(fd + 1, nullptr, &writefds, nullptr, &tv);
#endif
if (ret < 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (ret == 0 || !FD_ISSET(fd, &writefds)) {
return ConnectPollResult::CONNECT_POLL_PENDING;
}
int error = 0;
socklen_t len = sizeof(error);
if (sock.getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) != 0) {
err_out = errno;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
if (error != 0) {
err_out = error;
return ConnectPollResult::CONNECT_POLL_ERROR;
}
return ConnectPollResult::CONNECT_POLL_CONNECTED;
}
#endif
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port) {
#if USE_NETWORK_IPV6
if (addrlen < sizeof(sockaddr_in6)) {
-13
View File
@@ -145,19 +145,6 @@ inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const st
/// Set a sockaddr to the any address and specified port for the IP version used by socket_ip().
socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port);
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
enum class ConnectPollResult : uint8_t {
CONNECT_POLL_PENDING,
CONNECT_POLL_CONNECTED,
CONNECT_POLL_ERROR,
};
/// Check a non-blocking connect() for completion without blocking. On
/// CONNECT_POLL_ERROR, err_out holds the socket's SO_ERROR, or errno when the
/// poll itself failed.
ConnectPollResult poll_connect(Socket &sock, int &err_out);
#endif
/// Format sockaddr into caller-provided buffer, returns length written (excluding null)
size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::span<char, SOCKADDR_STR_LEN> buf);
+1 -1
View File
@@ -1335,7 +1335,7 @@ void Sprinkler::all_valves_off_(const bool include_pump) {
this->set_pump_state(this->valve_pump_switch(valve_index), false);
}
}
ESP_LOGD(TAG, "All valves stopped%s", include_pump ? LOG_STR_LITERAL(", including pumps") : "");
ESP_LOGD(TAG, "All valves stopped%s", include_pump ? ", including pumps" : "");
}
void Sprinkler::prep_full_cycle_() {
+1 -1
View File
@@ -1,6 +1,5 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor
from esphome.components.const import CONF_HOST
import esphome.config_validation as cv
from esphome.const import (
CONF_BINARY_SENSORS,
@@ -15,6 +14,7 @@ AUTO_LOAD = ["socket"]
CODEOWNERS = ["@Links2004"]
DEPENDENCIES = ["network"]
CONF_HOST = "host"
CONF_PREFIX = "prefix"
statsd_component_ns = cg.esphome_ns.namespace("statsd")
+1 -2
View File
@@ -26,8 +26,7 @@ void Switch::turn_off() {
this->write_state(this->inverted_);
}
void Switch::toggle() {
ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(),
this->state ? LOG_STR_LITERAL("OFF") : LOG_STR_LITERAL("ON"));
ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(), this->state ? "OFF" : "ON");
this->write_state(this->inverted_ == this->state);
}
optional<bool> Switch::get_initial_state() {

Some files were not shown because too many files have changed in this diff Show More