mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 20:16:01 +00:00
Compare commits
61
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
afc93a4dc8 | ||
|
|
0a74cb4e89 | ||
|
|
9c4a08aaad | ||
|
|
68d5968405 | ||
|
|
30da0e81fa | ||
|
|
900638463a | ||
|
|
1665d509d4 | ||
|
|
29d8f9c7c6 | ||
|
|
18b5277254 | ||
|
|
b110cee973 | ||
|
|
1171fbf892 | ||
|
|
ae549b25bb | ||
|
|
35ab455c10 | ||
|
|
dae9a5d1a6 | ||
|
|
bf380037a6 | ||
|
|
493c7265ab | ||
|
|
238b6bb0b7 | ||
|
|
0211bdf2c0 | ||
|
|
a95bfe0bcb | ||
|
|
3a0bbc4c17 | ||
|
|
cc9dc95cab | ||
|
|
081ef3d30d | ||
|
|
79325fe59a | ||
|
|
ca44db107c | ||
|
|
5fbe09b68c | ||
|
|
f41ff5aaeb | ||
|
|
d437869373 | ||
|
|
c20b619684 | ||
|
|
ebe6c2d049 | ||
|
|
78beb75b3b | ||
|
|
7b57fdd35c | ||
|
|
fc9afcb201 | ||
|
|
7a784d1135 | ||
|
|
24af3fd834 | ||
|
|
61e37cfc86 | ||
|
|
fc671d38e5 | ||
|
|
4e0cda0287 | ||
|
|
676eac7686 | ||
|
|
ea1e0c6f68 | ||
|
|
1ba1aebfa1 | ||
|
|
5d56517e14 | ||
|
|
ad69718ecc | ||
|
|
06d477bea7 | ||
|
|
bbe806f1e6 | ||
|
|
813c000684 | ||
|
|
2890afe0e5 | ||
|
|
5e58b312e2 | ||
|
|
575a540c9f | ||
|
|
404da26a37 | ||
|
|
be54a5c403 | ||
|
|
8b5c06add6 | ||
|
|
7b97c87390 | ||
|
|
0e4f460013 | ||
|
|
4e0a5e5a5b | ||
|
|
20d4fe1a41 | ||
|
|
8929fc43d8 | ||
|
|
c75252d425 | ||
|
|
ae460b430c | ||
|
|
82ca5365c9 | ||
|
|
a5d8c45b67 | ||
|
|
67f7532940 |
@@ -92,6 +92,7 @@ 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 }}
|
||||
@@ -152,6 +153,9 @@ 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
|
||||
@@ -427,8 +431,25 @@ 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 "${test_files[@]}"
|
||||
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
|
||||
- name: Print ccache statistics
|
||||
# esphome stores the PlatformIO ccache under the machine-global cache
|
||||
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
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@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
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 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
CHANNEL_DEV = "dev"
|
||||
@@ -64,7 +65,10 @@ def main():
|
||||
|
||||
suffix = f"-{args.suffix}" if args.suffix else ""
|
||||
|
||||
image_name = f"esphome/esphome{suffix}"
|
||||
repository = (
|
||||
(os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower()
|
||||
)
|
||||
image_name = f"{repository}{suffix}"
|
||||
|
||||
print(f"channel={channel}")
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -156,6 +157,17 @@ 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
|
||||
@@ -225,6 +237,17 @@ 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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -74,9 +74,7 @@ void ADCSensor::setup() {
|
||||
if (this->calibration_handle_ == nullptr) {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
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_;
|
||||
@@ -94,7 +92,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;
|
||||
}
|
||||
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = this->attenuation_,
|
||||
@@ -112,7 +110,11 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#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
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -121,23 +123,28 @@ void ADCSensor::setup() {
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
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");
|
||||
#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");
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -184,12 +191,11 @@ 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 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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -217,10 +223,9 @@ float ADCSensor::sample_autorange_() {
|
||||
// Need to recalibrate for the new attenuation
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif
|
||||
this->calibration_handle_ = nullptr;
|
||||
@@ -229,8 +234,7 @@ float ADCSensor::sample_autorange_() {
|
||||
// Create new calibration handle for this attenuation
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -242,7 +246,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);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = atten,
|
||||
@@ -264,10 +268,9 @@ float ADCSensor::sample_autorange_() {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
}
|
||||
@@ -286,10 +289,9 @@ 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 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
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ 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,
|
||||
@@ -56,6 +57,14 @@ 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"
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
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,6 +24,8 @@ from esphome.const import (
|
||||
CONF_CAPTURE_RESPONSE,
|
||||
CONF_DATA,
|
||||
CONF_DATA_TEMPLATE,
|
||||
CONF_DELAY,
|
||||
CONF_ENABLE_IPV6,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_EVENT,
|
||||
CONF_ID,
|
||||
@@ -47,6 +49,7 @@ from esphome.const import (
|
||||
)
|
||||
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
|
||||
|
||||
@@ -133,6 +136,7 @@ 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"
|
||||
|
||||
|
||||
@@ -284,9 +288,53 @@ 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
|
||||
# Platform default check here for a friendly early error; an explicit
|
||||
# lwip_tcp selection on other platforms is caught against the resolved
|
||||
# implementation in _validate_outgoing_socket_implementation
|
||||
if CORE.is_esp8266 or CORE.is_rp2:
|
||||
raise cv.Invalid(
|
||||
"outgoing_connection is not supported on this platform because its "
|
||||
"socket layer cannot make outgoing connections",
|
||||
path=[CONF_OUTGOING_CONNECTION],
|
||||
)
|
||||
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(
|
||||
{
|
||||
@@ -311,6 +359,7 @@ 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)),
|
||||
@@ -367,6 +416,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||
_validate_outgoing_connection,
|
||||
_consume_api_sockets,
|
||||
_register_provisioning_source,
|
||||
)
|
||||
@@ -423,7 +473,47 @@ def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
|
||||
def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType:
|
||||
"""A raw lwip_tcp socket can be selected explicitly on any platform."""
|
||||
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 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(
|
||||
@@ -606,6 +696,13 @@ 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)
|
||||
|
||||
@@ -992,6 +1089,7 @@ _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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -112,6 +112,11 @@ 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.
|
||||
@@ -331,6 +336,10 @@ 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 ====================
|
||||
|
||||
@@ -1822,6 +1822,19 @@ 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);
|
||||
}
|
||||
|
||||
@@ -1944,6 +1957,9 @@ 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;
|
||||
|
||||
@@ -375,6 +375,21 @@ 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);
|
||||
|
||||
@@ -745,6 +760,9 @@ 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
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
namespace esphome::api {
|
||||
|
||||
// uncomment to log raw packets
|
||||
//#define HELPER_LOG_PACKETS
|
||||
// #define HELPER_LOG_PACKETS
|
||||
|
||||
// Maximum message size limits to prevent OOM on constrained devices
|
||||
// Handshake messages are limited to a small size for security
|
||||
@@ -282,7 +282,8 @@ class APIFrameHelper {
|
||||
DATA = 5,
|
||||
CLOSED = 6,
|
||||
FAILED = 7,
|
||||
EXPLICIT_REJECT = 8, // Noise only
|
||||
EXPLICIT_REJECT = 8, // Noise only
|
||||
CLIENT_HELLO_OUTGOING = 9, // Noise only: like CLIENT_HELLO but the server hello already went out (outgoing conn)
|
||||
};
|
||||
|
||||
// Fast inline state check for read_packet/write_protobuf_messages hot path.
|
||||
|
||||
@@ -81,6 +81,13 @@ 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();
|
||||
@@ -253,6 +260,9 @@ 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_();
|
||||
@@ -285,11 +295,16 @@ 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::state_action_server_hello_() {
|
||||
// send server hello
|
||||
APIError APINoiseFrameHelper::send_server_hello_frame_() {
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
@@ -313,15 +328,18 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
|
||||
APIError aerr = write_frame_(msg, total_size);
|
||||
return write_frame_(msg, total_size);
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
APIError aerr = this->send_server_hello_frame_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
return this->start_handshake_();
|
||||
}
|
||||
APIError APINoiseFrameHelper::start_handshake_() {
|
||||
APIError aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,12 @@ 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;
|
||||
@@ -39,6 +45,8 @@ 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_();
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
#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_)) {
|
||||
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_ = {};
|
||||
}
|
||||
// Defend against a corrupt or truncated preference blob
|
||||
this->saved_.host[sizeof(this->saved_.host) - 1] = '\0';
|
||||
#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;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
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_(now);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,117 @@
|
||||
#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
|
||||
@@ -15,6 +15,11 @@ 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;
|
||||
}
|
||||
@@ -175,6 +180,9 @@ 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;
|
||||
}
|
||||
@@ -240,6 +248,9 @@ 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;
|
||||
}
|
||||
|
||||
@@ -412,13 +412,16 @@ class CommandProtoMessage : public ProtoDecodableMessage {
|
||||
class HelloRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 1;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 17;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#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
|
||||
@@ -549,7 +552,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 = 312;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 315;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
||||
#endif
|
||||
@@ -607,6 +610,9 @@ 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;
|
||||
|
||||
@@ -885,6 +885,9 @@ 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 {
|
||||
@@ -1008,6 +1011,9 @@ 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();
|
||||
}
|
||||
|
||||
@@ -34,7 +34,52 @@ 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_();
|
||||
#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();
|
||||
#endif
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
@@ -53,42 +98,14 @@ void APIServer::setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
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"));
|
||||
#ifdef USE_API_OUTGOING_CONNECTION
|
||||
// A dead listener degrades to an error status; dial-out still runs
|
||||
this->create_listen_socket_();
|
||||
#else
|
||||
if (!this->create_listen_socket_()) {
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
@@ -135,6 +152,9 @@ 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() {
|
||||
@@ -143,6 +163,12 @@ 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).
|
||||
@@ -151,7 +177,12 @@ 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_) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
// 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");
|
||||
}
|
||||
App.reboot();
|
||||
}
|
||||
}
|
||||
@@ -203,6 +234,15 @@ 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();
|
||||
|
||||
@@ -221,9 +261,18 @@ 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
|
||||
@@ -245,7 +294,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
||||
if (this->at_client_limit_()) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
@@ -254,18 +303,53 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
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();
|
||||
}
|
||||
this->add_client_(new APIConnection(std::move(sock), this));
|
||||
}
|
||||
}
|
||||
|
||||
bool APIServer::add_client_(APIConnection *conn) {
|
||||
if (this->at_client_limit_()) {
|
||||
// Callers check first; enforce the array bound where the write happens
|
||||
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: inbound clients may have filled the slots and
|
||||
// the PSK may have been cleared (mark_outgoing() needs the noise helper)
|
||||
const bool at_limit = this->at_client_limit_();
|
||||
if (at_limit || !this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGW(TAG, "Dropping outgoing connection (%s)", at_limit ? "max connections" : "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,
|
||||
@@ -282,6 +366,9 @@ 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) {}
|
||||
@@ -433,8 +520,10 @@ 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 ? "event" : "action", call.service.c_str(),
|
||||
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
|
||||
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"));
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -574,6 +663,8 @@ 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;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -685,6 +776,9 @@ 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;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#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"
|
||||
@@ -81,6 +82,10 @@ 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
|
||||
@@ -258,6 +263,16 @@ 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);
|
||||
|
||||
@@ -297,6 +312,7 @@ 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
|
||||
@@ -349,8 +365,16 @@ 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};
|
||||
bool shutting_down_ = false;
|
||||
// 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;
|
||||
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};
|
||||
@@ -360,6 +384,9 @@ 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)
|
||||
|
||||
@@ -23,8 +23,10 @@ void AQISensor::setup() {
|
||||
|
||||
void AQISensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AQI Sensor:");
|
||||
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");
|
||||
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"));
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
|
||||
}
|
||||
|
||||
@@ -42,7 +42,16 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(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;
|
||||
}
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
@@ -97,45 +106,22 @@ void AsyncClient::loop() {
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// 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) {
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*socket_, err)) {
|
||||
case socket::ConnectPollResult::CONNECT_POLL_PENDING:
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_CONNECTED:
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_ERROR:
|
||||
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
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);
|
||||
error_cb_(error_arg_, this, err);
|
||||
break;
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
|
||||
@@ -100,13 +100,15 @@ 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 ? "RELEASE" : "PRESS");
|
||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
|
||||
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("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 ? "RELEASE" : "PRESS");
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
|
||||
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
|
||||
@@ -162,8 +162,9 @@ 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 ? "Static" : "Mobile",
|
||||
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
|
||||
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"),
|
||||
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
|
||||
@@ -22,6 +22,7 @@ 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"
|
||||
|
||||
@@ -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 ? "negative" : "positive");
|
||||
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
|
||||
clear |= 1 << 22;
|
||||
}
|
||||
|
||||
@@ -319,7 +319,9 @@ void CS5460AComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CS5460A:\n"
|
||||
" Init status: %s",
|
||||
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
|
||||
state == COMPONENT_STATE_LOOP
|
||||
? LOG_STR_LITERAL("OK")
|
||||
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
|
||||
LOG_PIN(" CS Pin: ", cs_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Samples / cycle: %" PRIu32 "\n"
|
||||
@@ -330,9 +332,10 @@ 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 ? "50x" : "10x", current_gain_,
|
||||
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
|
||||
pulse_energy_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_);
|
||||
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", current_sensor_);
|
||||
LOG_SENSOR(" ", "Power", power_sensor_);
|
||||
|
||||
@@ -20,8 +20,8 @@ void DHT::dump_config() {
|
||||
"DHT:\n"
|
||||
" %sModel: %s\n"
|
||||
" Internal pull-up: %s",
|
||||
this->is_auto_detect_ ? "Auto-detected " : "",
|
||||
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
|
||||
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
|
||||
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
|
||||
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
|
||||
LOG_PIN(" Pin: ", this->t_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
@@ -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_ ? "DAC" : "PWM");
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
|
||||
if (this->dac_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
|
||||
} else {
|
||||
|
||||
@@ -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 ? "true" : "false");
|
||||
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
|
||||
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
|
||||
delay(ENS210_BOOTING_MS);
|
||||
return result;
|
||||
|
||||
@@ -5,11 +5,14 @@ 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 datasheet (page 96):
|
||||
# Per the ESP32-S31 IDF DOCS and datasheet:
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
_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}
|
||||
_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}
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
@@ -19,6 +22,8 @@ _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."
|
||||
@@ -33,6 +38,10 @@ 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,7 +128,8 @@ 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 : "version unknown");
|
||||
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
|
||||
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_OTA_PARTITIONS
|
||||
}
|
||||
@@ -357,7 +358,10 @@ 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));
|
||||
this->client_->setblocking(true);
|
||||
if (this->client_->setblocking(true) != 0) {
|
||||
this->log_socket_error_(LOG_STR("blocking"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
@@ -93,7 +93,8 @@ 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 ? "STARTED" : "ENDED");
|
||||
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
|
||||
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
|
||||
this->endstop_reached_(true);
|
||||
@@ -106,7 +107,8 @@ 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 ? "STARTED" : "ENDED");
|
||||
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
|
||||
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
|
||||
this->endstop_reached_(false);
|
||||
@@ -144,7 +146,8 @@ 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 ? "Open" : "Close", dur);
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
|
||||
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
|
||||
|
||||
// if there is no external mechanism, stop the cover
|
||||
if (!this->has_built_in_endstop_) {
|
||||
@@ -366,7 +369,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 ? "Open" : "Close");
|
||||
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -383,9 +386,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 ? "OPEN"
|
||||
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
|
||||
: "STOP");
|
||||
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
|
||||
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
|
||||
: LOG_STR_LITERAL("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 : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
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"));
|
||||
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,18 +35,20 @@ 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 ? "Rotary" : "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 ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("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,10 +69,12 @@ 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 ? "HIGH" : "LOW");
|
||||
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
} else {
|
||||
// IO expander pin: leave as output for configuration only.
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,8 @@ void HaierClimateBase::setup() {
|
||||
|
||||
void HaierClimateBase::dump_config() {
|
||||
LOG_CLIMATE("", "Haier Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s",
|
||||
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
|
||||
}
|
||||
|
||||
void HaierClimateBase::loop() {
|
||||
|
||||
@@ -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] ? " 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" : ""));
|
||||
(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") : ""));
|
||||
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HDC302x:\n"
|
||||
" Heater: %s",
|
||||
this->heater_active_ ? "active" : "inactive");
|
||||
this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive"));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
|
||||
|
||||
@@ -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 ? "Open" : "Close", dur);
|
||||
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("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 ? "OPEN"
|
||||
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
|
||||
: "STOP");
|
||||
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
|
||||
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
|
||||
: LOG_STR_LITERAL("STOP"));
|
||||
|
||||
if (dir == this->next_direction_) {
|
||||
// either moving and needs to stop, or stopped and will move correctly on one trigger
|
||||
|
||||
@@ -336,7 +336,8 @@ 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 ? "ON" : "OFF");
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s",
|
||||
this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
|
||||
}
|
||||
if (this->face_count_sensor_) {
|
||||
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
|
||||
|
||||
@@ -97,7 +97,8 @@ 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_ ? "CURRENT" : "VOLTAGE");
|
||||
ESP_LOGV(TAG, "Changing mode to %s mode",
|
||||
this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE"));
|
||||
this->change_mode_at_ = 0;
|
||||
this->sel_pin_->digital_write(this->current_mode_);
|
||||
}
|
||||
|
||||
@@ -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 ? "BGR" : "RGB", YESNO(this->swap_xy_),
|
||||
YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
|
||||
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_));
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["improv_base"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger", "wifi"]
|
||||
DEPENDENCIES = ["logger", "network"]
|
||||
|
||||
improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "improv_serial_component.h"
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -7,7 +7,10 @@
|
||||
#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>
|
||||
|
||||
@@ -26,13 +29,17 @@ void ImprovSerialComponent::setup() {
|
||||
this->hw_serial_ = logger::global_logger->get_hw_serial();
|
||||
#endif
|
||||
|
||||
if (wifi::global_wifi_component->has_sta()) {
|
||||
// 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()) {
|
||||
this->state_ = improv::STATE_PROVISIONED;
|
||||
} else if (!wifi::global_wifi_component->is_disabled()) {
|
||||
} else if (wifi::global_wifi_component != nullptr && !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() {
|
||||
@@ -55,8 +62,14 @@ void ImprovSerialComponent::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
#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) {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
@@ -66,6 +79,7 @@ void ImprovSerialComponent::loop() {
|
||||
this->send_settings_response_(improv::WIFI_SETTINGS);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
||||
@@ -143,15 +157,17 @@ 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
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
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;
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ip.str_to(ip_buf);
|
||||
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
|
||||
@@ -162,9 +178,43 @@ void ImprovSerialComponent::send_settings_response_(improv::Command command) {
|
||||
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));
|
||||
}
|
||||
@@ -231,7 +281,8 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
||||
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
|
||||
switch (command.command) {
|
||||
case improv::WIFI_SETTINGS: {
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component == nullptr || 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");
|
||||
@@ -243,21 +294,32 @@ 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", 30000, [this]() { this->on_wifi_connect_timeout_(); });
|
||||
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
|
||||
return true;
|
||||
}
|
||||
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.
|
||||
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.
|
||||
this->send_current_state_(improv::STATE_STOPPED);
|
||||
return true;
|
||||
}
|
||||
@@ -265,14 +327,20 @@ 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: {
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
// 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();
|
||||
for (const auto &scan : results) {
|
||||
bool with_auth = false;
|
||||
if (!wifi::should_show_scan_entry(results, scan, with_auth))
|
||||
@@ -289,11 +357,52 @@ 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);
|
||||
@@ -331,12 +440,14 @@ 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,15 +2,19 @@
|
||||
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#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)
|
||||
@@ -48,13 +52,22 @@ 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
|
||||
// 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.
|
||||
static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5;
|
||||
#else
|
||||
static constexpr size_t WEBSERVER_URL_RESERVE = 0;
|
||||
@@ -84,8 +97,15 @@ 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_();
|
||||
|
||||
@@ -167,7 +187,9 @@ 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,7 +209,8 @@ 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_ ? "±40.96 mV" : "±163.84 mV", this->current_lsb_,
|
||||
(uint8_t) this->adc_range_,
|
||||
this->adc_range_ ? LOG_STR_LITERAL("±40.96 mV") : LOG_STR_LITERAL("±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",
|
||||
|
||||
@@ -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_ ? "grayscale" : "mono");
|
||||
this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1063,25 +1063,27 @@ 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_ : "(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_);
|
||||
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_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
|
||||
@@ -150,7 +150,8 @@ 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 ? "8" : "7");
|
||||
this->pack_size_, this->apa_,
|
||||
this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7"));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
|
||||
@@ -701,7 +701,8 @@ 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 ? "enable" : "disable", cmd_frame.command);
|
||||
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
|
||||
cmd_frame.command);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -437,7 +437,8 @@ void LD6002BComponent::dump_config() {
|
||||
"HLK-LD6002B:\n"
|
||||
" Auto wake: %s\n"
|
||||
" Max data length: %u",
|
||||
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
|
||||
this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("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_);
|
||||
|
||||
@@ -628,10 +628,6 @@ 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)
|
||||
|
||||
|
||||
@@ -23,8 +23,10 @@ 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 ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
|
||||
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!"))));
|
||||
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
|
||||
ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown");
|
||||
} else {
|
||||
|
||||
@@ -80,12 +80,14 @@ 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 ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "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 ? LOG_STR_LITERAL("60 Hz")
|
||||
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
|
||||
}
|
||||
|
||||
void MAX31865Sensor::read_data_() {
|
||||
|
||||
@@ -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) ? "nonvolatile " : "", wiper_idx);
|
||||
ESP_LOGW(TAG, "Error fetching %swiper %u value", (wiper_idx > 3) ? LOG_STR_LITERAL("nonvolatile ") : "", wiper_idx);
|
||||
return 0;
|
||||
}
|
||||
if (ok != nullptr) {
|
||||
@@ -377,7 +377,8 @@ 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) ? "nonvolatile " : "", wiper, value);
|
||||
ESP_LOGW(TAG, "Error writing %swiper %u level %u", (wiper > 3) ? LOG_STR_LITERAL("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() ? "yes" : "no");
|
||||
ESP_LOGV(TAG, " Announcement: %s", this->announcement_.value() ? LOG_STR_LITERAL("yes") : LOG_STR_LITERAL("no"));
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,12 @@ 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, only_on_variant
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
only_on_variant,
|
||||
)
|
||||
from esphome.components.mipi import (
|
||||
COLOR_ORDERS,
|
||||
CONF_DE_PIN,
|
||||
@@ -226,7 +231,7 @@ def _config_schema(config: ConfigType) -> ConfigType:
|
||||
config = cv.All(
|
||||
schema,
|
||||
cv.only_on_esp32,
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4]),
|
||||
only_on_variant(supported=[VARIANT_ESP32S3, VARIANT_ESP32P4, VARIANT_ESP32S31]),
|
||||
)(config)
|
||||
model = MODELS[config[CONF_MODEL].upper()]
|
||||
model.check_requirements()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#include "mipi_rgb.h"
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -400,4 +400,5 @@ void MipiRgb::dump_config() {
|
||||
}
|
||||
|
||||
} // namespace esphome::mipi_rgb
|
||||
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#endif // defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) ||
|
||||
// defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
|
||||
#if defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S31)
|
||||
#include "esphome/core/gpio.h"
|
||||
#include "esphome/components/display/display.h"
|
||||
#include "esp_lcd_panel_ops.h"
|
||||
|
||||
@@ -25,7 +25,8 @@ 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) ? "BGR" : "RGB", display_bits, is_big_endian ? "Big" : "Little", spi_mode,
|
||||
(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,
|
||||
static_cast<unsigned>(data_rate / 1000000), bus_width);
|
||||
LOG_PIN(" CS Pin: ", cs);
|
||||
LOG_PIN(" Reset Pin: ", reset);
|
||||
|
||||
@@ -269,7 +269,8 @@ void ModbusServer::dump_config() {
|
||||
" Enabled: %s\n"
|
||||
" Register Last Address: 0x%02X\n"
|
||||
" Register Value: %" PRIu16,
|
||||
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
|
||||
this->address_,
|
||||
this->server_courtesy_response_.enabled ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
|
||||
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
@@ -280,8 +281,9 @@ 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 ? "true" : "false",
|
||||
b->write_lambda ? "true" : "false");
|
||||
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"));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -595,7 +595,8 @@ 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 ? "PRESS" : "RELEASE", page_id, component_id);
|
||||
ESP_LOGV(TAG, "Touch %s: page %u comp %u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"),
|
||||
page_id, component_id);
|
||||
for (auto *touch : this->touch_) {
|
||||
touch->process_touch(page_id, component_id, touch_event != 0);
|
||||
}
|
||||
@@ -628,7 +629,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 ? "PRESS" : "RELEASE", x, y);
|
||||
ESP_LOGV(TAG, "Touch %s at %u,%u", touch_event ? LOG_STR_LITERAL("PRESS") : LOG_STR_LITERAL("RELEASE"), x, y);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -119,7 +119,8 @@ 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 ? "0 dB" : "-6 dB",
|
||||
this->bits_per_sample_,
|
||||
this->analog_gain_ == PCM5122_ANALOG_GAIN_0DB ? LOG_STR_LITERAL("0 dB") : LOG_STR_LITERAL("-6 dB"),
|
||||
channel_mix_str, this->volume_min_db_, this->volume_max_db_, YESNO(this->is_muted_));
|
||||
LOG_PIN(" Enable Pin: ", this->enable_pin_);
|
||||
}
|
||||
|
||||
@@ -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] ? "reset" : "retained",
|
||||
rx.get_message()[nfc::NCI_PKT_PAYLOAD_OFFSET + 1] == 0x20 ? "2.0" : "1.0");
|
||||
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"));
|
||||
|
||||
return nfc::STATUS_OK;
|
||||
}
|
||||
|
||||
@@ -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] ? "reset" : "retained", rx.get_message()[5] == 0x20 ? "2.0" : "1.0",
|
||||
rx.get_message()[6]);
|
||||
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().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()));
|
||||
|
||||
@@ -137,7 +137,8 @@ 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_ ? "with" : "without",
|
||||
ESP_LOGD(TAG, "header line %s Tlow.Id: %s",
|
||||
this->has_tlow_id_ ? LOG_STR_LITERAL("with") : LOG_STR_LITERAL("without"),
|
||||
buffer.substr(0, buffer.size() - 2).c_str());
|
||||
return;
|
||||
} else {
|
||||
|
||||
@@ -55,8 +55,9 @@ 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 ? "single" : "multi");
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " Tracking Mode: %s",
|
||||
*this->tracking_mode_ == TrackingMode::SINGLE_TARGET ? LOG_STR_LITERAL("single") : LOG_STR_LITERAL("multi"));
|
||||
}
|
||||
if (this->throttle_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Throttle: %" PRIu32 "ms", this->throttle_);
|
||||
|
||||
@@ -76,15 +76,16 @@ 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) ? " us" : "%", 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) ? LOG_STR_LITERAL(" us") : LOG_STR_LITERAL("%"),
|
||||
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 ? "UPSTREAM" : "DOWNSTREAM", this->resistor_,
|
||||
this->reference_voltage_);
|
||||
this->configuration_ == UPSTREAM ? LOG_STR_LITERAL("UPSTREAM") : LOG_STR_LITERAL("DOWNSTREAM"),
|
||||
this->resistor_, this->reference_voltage_);
|
||||
}
|
||||
void ResistanceSensor::process_(float value) {
|
||||
if (std::isnan(value)) {
|
||||
|
||||
@@ -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() ? "FAILED" : "OK");
|
||||
ESP_LOGI(TAG, "SEN21231: %s", this->is_failed() ? LOG_STR_LITERAL("FAILED") : LOG_STR_LITERAL("OK"));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,9 +9,11 @@ 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
|
||||
// Single numeric timeout ID — the chain is sequential so only one is active at a time.
|
||||
// Numeric timeout IDs. Each chain is sequential, so only one timeout per ID 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;
|
||||
@@ -26,6 +28,8 @@ 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;
|
||||
@@ -143,21 +147,76 @@ 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_);
|
||||
|
||||
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");
|
||||
// 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_(); });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
@@ -1,11 +1,25 @@
|
||||
#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)
|
||||
@@ -27,22 +41,46 @@ 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_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
std::string serial_number_;
|
||||
optional<GasTuning> voc_tuning_params_;
|
||||
optional<GasTuning> nox_tuning_params_;
|
||||
Sen6xType sen6x_type_{UNKNOWN};
|
||||
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};
|
||||
};
|
||||
|
||||
|
||||
@@ -3,15 +3,22 @@ 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,
|
||||
@@ -44,6 +51,42 @@ 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"),
|
||||
@@ -94,15 +137,15 @@ CONFIG_SCHEMA = cv.All(
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VOC_INDEX): sensor.sensor_schema(
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
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_NOX_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_CO2): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_MILLION,
|
||||
@@ -149,3 +192,20 @@ 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))
|
||||
|
||||
@@ -89,8 +89,9 @@ 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 ? "OK" : "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 ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("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 ? "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");
|
||||
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"));
|
||||
}
|
||||
|
||||
SerialProxyResult SerialProxy::configure(api::APIConnection *api_connection, uint32_t baudrate, bool flow_control,
|
||||
|
||||
@@ -284,7 +284,7 @@ void SGP4xComponent::dump_config() {
|
||||
" Type: %s\n"
|
||||
" Serial number: %" PRIu64 "\n"
|
||||
" Minimum Samples: %f",
|
||||
this->sgp_type_ == SGP41 ? "SGP41" : "SGP40", this->serial_number_,
|
||||
this->sgp_type_ == SGP41 ? LOG_STR_LITERAL("SGP41") : LOG_STR_LITERAL("SGP40"), this->serial_number_,
|
||||
GasIndexAlgorithm_INITIAL_BLACKOUT);
|
||||
}
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
@@ -17,6 +17,8 @@ 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,13 +59,15 @@ 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;
|
||||
}
|
||||
::fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return ::fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t BSDSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -49,13 +49,15 @@ 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;
|
||||
}
|
||||
lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
return 0;
|
||||
return lwip_fcntl(this->fd_, F_SETFL, fl);
|
||||
}
|
||||
|
||||
size_t LwIPSocketImpl::getpeername_to(std::span<char, SOCKADDR_STR_LEN> buf) {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#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"
|
||||
@@ -165,7 +168,10 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
#else
|
||||
// Use LWIP-specific functions
|
||||
ip6_addr_t ip6;
|
||||
inet6_aton(ip_address, &ip6);
|
||||
if (inet6_aton(ip_address, &ip6) == 0) {
|
||||
errno = EINVAL;
|
||||
return 0;
|
||||
}
|
||||
memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr));
|
||||
#endif
|
||||
return sizeof(sockaddr_in6);
|
||||
@@ -185,12 +191,58 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_
|
||||
return 0;
|
||||
}
|
||||
#else
|
||||
server->sin_addr.s_addr = inet_addr(ip_address);
|
||||
// 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;
|
||||
}
|
||||
#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)) {
|
||||
|
||||
@@ -145,6 +145,19 @@ 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);
|
||||
|
||||
|
||||
@@ -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 ? ", including pumps" : "");
|
||||
ESP_LOGD(TAG, "All valves stopped%s", include_pump ? LOG_STR_LITERAL(", including pumps") : "");
|
||||
}
|
||||
|
||||
void Sprinkler::prep_full_cycle_() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,
|
||||
@@ -14,7 +15,6 @@ AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@Links2004"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONF_HOST = "host"
|
||||
CONF_PREFIX = "prefix"
|
||||
|
||||
statsd_component_ns = cg.esphome_ns.namespace("statsd")
|
||||
|
||||
@@ -26,7 +26,8 @@ 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 ? "OFF" : "ON");
|
||||
ESP_LOGV(TAG, "'%s' Toggling %s.", this->get_name().c_str(),
|
||||
this->state ? LOG_STR_LITERAL("OFF") : LOG_STR_LITERAL("ON"));
|
||||
this->write_state(this->inverted_ == this->state);
|
||||
}
|
||||
optional<bool> Switch::get_initial_state() {
|
||||
|
||||
@@ -479,8 +479,9 @@ void SX127x::dump_config() {
|
||||
" Rx Start: %s\n"
|
||||
" Rx Floor: %.1f dBm\n"
|
||||
" Packet Mode: %s",
|
||||
shaping, this->modulation_ == MOD_FSK ? "FSK" : "OOK", this->bitrate_, TRUEFALSE(this->bitsync_),
|
||||
TRUEFALSE(this->rx_start_), this->rx_floor_, TRUEFALSE(this->packet_mode_));
|
||||
shaping, this->modulation_ == MOD_FSK ? LOG_STR_LITERAL("FSK") : LOG_STR_LITERAL("OOK"),
|
||||
this->bitrate_, TRUEFALSE(this->bitsync_), TRUEFALSE(this->rx_start_), this->rx_floor_,
|
||||
TRUEFALSE(this->packet_mode_));
|
||||
if (this->packet_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " CRC Enable: %s", TRUEFALSE(this->crc_enable_));
|
||||
}
|
||||
|
||||
@@ -1432,7 +1432,8 @@ void ThermostatClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" On boot, restore from: %s\n"
|
||||
" Use Start-up Delay: %s",
|
||||
this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? "DEFAULT_PRESET" : "MEMORY",
|
||||
this->on_boot_restore_from_ == thermostat::DEFAULT_PRESET ? LOG_STR_LITERAL("DEFAULT_PRESET")
|
||||
: LOG_STR_LITERAL("MEMORY"),
|
||||
YESNO(this->use_startup_delay_));
|
||||
if (this->supports_two_points_) {
|
||||
ESP_LOGCONFIG(TAG, " Minimum Set Point Differential: %.1f°C", this->set_point_minimum_differential_);
|
||||
@@ -1550,7 +1551,8 @@ void ThermostatClimate::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Supported PRESETS:");
|
||||
for (const auto &entry : this->preset_config_) {
|
||||
const auto *preset_name = LOG_STR_ARG(climate::climate_preset_to_string(entry.preset));
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name, entry.preset == this->default_preset_ ? " (default)" : "");
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
|
||||
entry.preset == this->default_preset_ ? LOG_STR_LITERAL(" (default)") : "");
|
||||
this->dump_preset_config_(preset_name, entry.config);
|
||||
}
|
||||
}
|
||||
@@ -1561,7 +1563,7 @@ void ThermostatClimate::dump_config() {
|
||||
const auto *preset_name = entry.name;
|
||||
ESP_LOGCONFIG(TAG, " %s:%s", preset_name,
|
||||
(this->default_custom_preset_ != nullptr && strcmp(entry.name, this->default_custom_preset_) == 0)
|
||||
? " (default)"
|
||||
? LOG_STR_LITERAL(" (default)")
|
||||
: "");
|
||||
this->dump_preset_config_(preset_name, entry.config);
|
||||
}
|
||||
|
||||
@@ -269,7 +269,7 @@ uint32_t TSL2591Component::get_combined_illuminance() {
|
||||
break;
|
||||
}
|
||||
// we only log this if we need any delay, since normally we don't
|
||||
ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? "true" : "false");
|
||||
ESP_LOGD(TAG, " after %3d ms: ADC valid? %s", d, avalid ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
|
||||
delay(mini_delay);
|
||||
}
|
||||
if (!avalid) {
|
||||
|
||||
@@ -39,7 +39,7 @@ void TuyaSelect::dump_config() {
|
||||
" Select has datapoint ID %u\n"
|
||||
" Data type: %s\n"
|
||||
" Options are:",
|
||||
this->select_id_, this->is_int_ ? "int" : "enum");
|
||||
this->select_id_, this->is_int_ ? LOG_STR_LITERAL("int") : LOG_STR_LITERAL("enum"));
|
||||
const auto &options = this->traits.get_options();
|
||||
for (size_t i = 0; i < this->mappings_.size(); i++) {
|
||||
ESP_LOGCONFIG(TAG, " %i: %s", this->mappings_.at(i), options.at(i));
|
||||
|
||||
@@ -259,7 +259,7 @@ void Tuya::handle_command_(uint8_t command, uint8_t version, const uint8_t *buff
|
||||
st.payload[0] = 0x04;
|
||||
this->send_command_(st);
|
||||
ESP_LOGI(TAG, "%s received (%s), replied with WIFI_STATE confirming connection established",
|
||||
is_select ? "WIFI_SELECT" : "WIFI_RESET", mode_str);
|
||||
is_select ? LOG_STR_LITERAL("WIFI_SELECT") : LOG_STR_LITERAL("WIFI_RESET"), mode_str);
|
||||
break;
|
||||
}
|
||||
case TuyaCommandType::DATAPOINT_DELIVER:
|
||||
|
||||
@@ -13,9 +13,16 @@ void UDPComponent::setup() {
|
||||
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
|
||||
for (const auto &address : this->addresses_) {
|
||||
struct sockaddr saddr {};
|
||||
socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_);
|
||||
if (socket::set_sockaddr(&saddr, sizeof(saddr), address, this->broadcast_port_) == 0) {
|
||||
ESP_LOGW(TAG, "Invalid address %s", address);
|
||||
continue;
|
||||
}
|
||||
this->sockaddrs_.push_back(saddr);
|
||||
}
|
||||
if (this->sockaddrs_.size() != this->addresses_.size()) {
|
||||
// A dropped address silently receives nothing; surface the misconfiguration
|
||||
this->status_set_warning(LOG_STR("invalid address"));
|
||||
}
|
||||
// set up broadcast socket
|
||||
if (this->should_broadcast_) {
|
||||
this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
|
||||
|
||||
@@ -259,7 +259,7 @@ ErrorCode VEML7700Component::configure_() {
|
||||
|
||||
ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Gain gain, bool shutdown) {
|
||||
ESP_LOGV(TAG, "Reconfigure time and gain (%d ms, %s) %s", get_itime_ms(time), get_gain_str(gain),
|
||||
shutdown ? "Shutting down" : "Turning back on");
|
||||
shutdown ? LOG_STR_LITERAL("Shutting down") : LOG_STR_LITERAL("Turning back on"));
|
||||
|
||||
ConfigurationRegister als_conf{0};
|
||||
als_conf.raw = 0;
|
||||
@@ -272,7 +272,7 @@ ErrorCode VEML7700Component::reconfigure_time_and_gain_(IntegrationTime time, Ga
|
||||
als_conf.ALS_GAIN = gain;
|
||||
auto err = this->write_register((uint8_t) CommandRegisters::ALS_CONF_0, als_conf.raw_bytes, VEML_REG_SIZE);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "%s failed", shutdown ? "Shutdown" : "Turn on");
|
||||
ESP_LOGW(TAG, "%s failed", shutdown ? LOG_STR_LITERAL("Shutdown") : LOG_STR_LITERAL("Turn on"));
|
||||
}
|
||||
|
||||
return err;
|
||||
@@ -363,8 +363,8 @@ void VEML7700Component::apply_lux_calculation_(Readings &data) {
|
||||
data.fake_infrared_lux = reduce_to_zero(data.white_lux, data.als_lux);
|
||||
|
||||
ESP_LOGV(TAG, "%s mode - ALS = %.1f lx, WHITE = %.1f lx, FAKE_IR = %.1f lx",
|
||||
this->automatic_mode_enabled_ ? "Automatic" : "Manual", data.als_lux, data.white_lux,
|
||||
data.fake_infrared_lux);
|
||||
this->automatic_mode_enabled_ ? LOG_STR_LITERAL("Automatic") : LOG_STR_LITERAL("Manual"), data.als_lux,
|
||||
data.white_lux, data.fake_infrared_lux);
|
||||
}
|
||||
|
||||
void VEML7700Component::apply_lux_compensation_(Readings &data) {
|
||||
|
||||
@@ -31,7 +31,8 @@ void VL53L0XSensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Timeout: %" PRIu32 "%s\n"
|
||||
" Timing Budget %" PRIu32 "us ",
|
||||
this->timeout_us_, this->timeout_us_ > 0 ? "us" : " (no timeout)", this->measurement_timing_budget_us_);
|
||||
this->timeout_us_, this->timeout_us_ > 0 ? LOG_STR_LITERAL("us") : LOG_STR_LITERAL(" (no timeout)"),
|
||||
this->measurement_timing_budget_us_);
|
||||
}
|
||||
|
||||
void VL53L0XSensor::setup() {
|
||||
|
||||
@@ -34,6 +34,10 @@ void WakeOnLanButton::press_action() {
|
||||
struct sockaddr_storage saddr {};
|
||||
auto addr_len =
|
||||
socket::set_sockaddr(reinterpret_cast<sockaddr *>(&saddr), sizeof(saddr), "255.255.255.255", this->port_);
|
||||
if (addr_len == 0) {
|
||||
ESP_LOGW(TAG, "Invalid broadcast address");
|
||||
return;
|
||||
}
|
||||
uint8_t buffer[6 + sizeof this->macaddr_ * 16];
|
||||
memcpy(buffer, PREFIX, sizeof(PREFIX));
|
||||
for (size_t i = 0; i != 16; i++) {
|
||||
|
||||
@@ -100,10 +100,11 @@ void WaterHeaterCall::perform() {
|
||||
ESP_LOGV(TAG, " Target Temperature High: %.2f", this->target_temperature_high_);
|
||||
}
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_AWAY) {
|
||||
ESP_LOGV(TAG, " Away: %s", (this->state_ & WATER_HEATER_STATE_AWAY) ? "YES" : "NO");
|
||||
ESP_LOGV(TAG, " Away: %s",
|
||||
(this->state_ & WATER_HEATER_STATE_AWAY) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
|
||||
}
|
||||
if (this->state_mask_ & WATER_HEATER_STATE_ON) {
|
||||
ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO");
|
||||
ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
|
||||
}
|
||||
this->parent_->control(*this);
|
||||
}
|
||||
@@ -178,7 +179,7 @@ void WaterHeater::publish_state() {
|
||||
ESP_LOGV(TAG, " Away: YES");
|
||||
}
|
||||
if (traits.has_feature_flags(WATER_HEATER_SUPPORTS_ON_OFF)) {
|
||||
ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? "YES" : "NO");
|
||||
ESP_LOGV(TAG, " On: %s", (this->state_ & WATER_HEATER_STATE_ON) ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
|
||||
}
|
||||
|
||||
#if defined(USE_WATER_HEATER) && defined(USE_CONTROLLER_REGISTRY)
|
||||
|
||||
@@ -134,7 +134,7 @@ void WeikaiComponent::loop() {
|
||||
}
|
||||
bool status = children_[i]->uart_receive_test_(message);
|
||||
ESP_LOGI(TAG, "Test %s => send/received %u bytes %s - execution time %" PRIu32 " ms", message, RING_BUFFER_SIZE,
|
||||
status ? "correctly" : "with error", elapsed_ms(time));
|
||||
status ? LOG_STR_LITERAL("correctly") : LOG_STR_LITERAL("with error"), elapsed_ms(time));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,9 +238,9 @@ void WeikaiComponent::set_pin_direction_(uint8_t pin, gpio::Flags flags) {
|
||||
|
||||
void WeikaiGPIOPin::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting GPIO pin %d mode to %s", this->pin_,
|
||||
flags_ == gpio::FLAG_INPUT ? "Input"
|
||||
: this->flags_ == gpio::FLAG_OUTPUT ? "Output"
|
||||
: "NOT SPECIFIED");
|
||||
this->flags_ == gpio::FLAG_INPUT ? LOG_STR_LITERAL("Input")
|
||||
: this->flags_ == gpio::FLAG_OUTPUT ? LOG_STR_LITERAL("Output")
|
||||
: LOG_STR_LITERAL("NOT SPECIFIED"));
|
||||
this->pin_mode(this->flags_);
|
||||
}
|
||||
|
||||
@@ -420,7 +420,7 @@ bool WeikaiChannel::read_array(uint8_t *buffer, size_t length) {
|
||||
this->receive_buffer_.pop(buffer[i]);
|
||||
}
|
||||
ESP_LOGVV(TAG, "read_array(ch=%d buffer[0]=%02X, length=%d): status %s", this->channel_, *buffer, length,
|
||||
status ? "OK" : "ERROR");
|
||||
status ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("ERROR"));
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -558,8 +558,8 @@ bool WeikaiChannel::uart_receive_test_(char *message) {
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "%s => received %d bytes status %s - exec time %d µs", message, received, status ? "OK" : "ERROR",
|
||||
micros() - start_exec);
|
||||
ESP_LOGV(TAG, "%s => received %d bytes status %s - exec time %d µs", message, received,
|
||||
status ? LOG_STR_LITERAL("OK") : LOG_STR_LITERAL("ERROR"), micros() - start_exec);
|
||||
return status;
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ void WhirlpoolClimate::transmit_state() {
|
||||
}
|
||||
|
||||
// Swing
|
||||
ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? "true" : "false");
|
||||
ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
|
||||
if (this->send_swing_cmd_) {
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_OFF) {
|
||||
remote_state[2] |= 128;
|
||||
|
||||
@@ -84,8 +84,8 @@ void Whynter::transmit_state() {
|
||||
|
||||
if (fahrenheit_) {
|
||||
remote_state |= UNIT_MASK;
|
||||
uint8_t temp =
|
||||
(uint8_t) clamp<float>(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F);
|
||||
uint8_t temp = (uint8_t) roundf(
|
||||
clamp<float>(esphome::celsius_to_fahrenheit(this->target_temperature), TEMP_MIN_F, TEMP_MAX_F));
|
||||
temp = esphome::reverse_bits(temp);
|
||||
remote_state |= temp;
|
||||
} else {
|
||||
|
||||
@@ -933,7 +933,7 @@ void WiFiComponent::loop() {
|
||||
if (semaphore_count > 0 && !this->is_high_performance_mode_) {
|
||||
// Transition to high-performance mode (no power save)
|
||||
ESP_LOGV(TAG, "Switching to high-performance mode (%" PRIu32 " active %s)", (uint32_t) semaphore_count,
|
||||
semaphore_count == 1 ? "request" : "requests");
|
||||
semaphore_count == 1 ? LOG_STR_LITERAL("request") : LOG_STR_LITERAL("requests"));
|
||||
this->power_save_ = WIFI_POWER_SAVE_NONE;
|
||||
if (this->wifi_apply_power_save_()) {
|
||||
this->is_high_performance_mode_ = true;
|
||||
@@ -1181,8 +1181,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) {
|
||||
" CA Cert: %s\n"
|
||||
" Client Cert: %s\n"
|
||||
" Client Key: %s",
|
||||
ca_cert_present ? "present" : "not present", client_cert_present ? "present" : "not present",
|
||||
client_key_present ? "present" : "not present");
|
||||
ca_cert_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present"),
|
||||
client_cert_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present"),
|
||||
client_key_present ? LOG_STR_LITERAL("present") : LOG_STR_LITERAL("not present"));
|
||||
} else {
|
||||
#endif
|
||||
ESP_LOGV(TAG, " Password: " LOG_SECRET("'%s'"), ap.password_.c_str());
|
||||
@@ -1316,7 +1317,8 @@ void WiFiComponent::print_connect_params_() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" BTM: %s\n"
|
||||
" RRM: %s",
|
||||
this->btm_ ? "enabled" : "disabled", this->rrm_ ? "enabled" : "disabled");
|
||||
this->btm_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
|
||||
this->rrm_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -146,18 +146,19 @@ void Wireguard::dump_config() {
|
||||
" Peer Pre-shared Key: " LOG_SECRET("%s"),
|
||||
this->address_, this->netmask_, private_key_masked,
|
||||
this->peer_endpoint_, this->peer_port_, this->peer_public_key_,
|
||||
(this->preshared_key_ != nullptr ? preshared_key_masked : "NOT IN USE"));
|
||||
(this->preshared_key_ != nullptr ? preshared_key_masked : LOG_STR_LITERAL("NOT IN USE")));
|
||||
// clang-format on
|
||||
ESP_LOGCONFIG(TAG, " Peer Allowed IPs:");
|
||||
for (const AllowedIP &allowed_ip : this->allowed_ips_) {
|
||||
ESP_LOGCONFIG(TAG, " - %s/%s", allowed_ip.ip, allowed_ip.netmask);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Peer Persistent Keepalive: %d%s", this->keepalive_,
|
||||
(this->keepalive_ > 0 ? "s" : " (DISABLED)"));
|
||||
(this->keepalive_ > 0 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(" (DISABLED)")));
|
||||
ESP_LOGCONFIG(TAG, " Reboot Timeout: %" PRIu32 "%s", (this->reboot_timeout_ / 1000),
|
||||
(this->reboot_timeout_ != 0 ? "s" : " (DISABLED)"));
|
||||
(this->reboot_timeout_ != 0 ? LOG_STR_LITERAL("s") : LOG_STR_LITERAL(" (DISABLED)")));
|
||||
// be careful: if proceed_allowed_ is true, require connection is false
|
||||
ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s", (this->proceed_allowed_ ? "NO" : "YES"));
|
||||
ESP_LOGCONFIG(TAG, " Require Connection to Proceed: %s",
|
||||
(this->proceed_allowed_ ? LOG_STR_LITERAL("NO") : LOG_STR_LITERAL("YES")));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,8 @@ Wl134Component::Rfid134Error Wl134Component::read_packet_() {
|
||||
" isAnimal: %s\n"
|
||||
" Reserved0: %d\n"
|
||||
" Reserved1: %" PRId32,
|
||||
reading.id, reading.country, reading.isData ? "true" : "false", reading.isAnimal ? "true" : "false",
|
||||
reading.reserved0, reading.reserved1);
|
||||
reading.id, reading.country, reading.isData ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
|
||||
reading.isAnimal ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"), reading.reserved0, reading.reserved1);
|
||||
|
||||
char buf[20]; // "%03d" (3) + "%012" PRId64 (12) + null = 16 max
|
||||
buf_append_printf(buf, sizeof(buf), 0, "%03d%012" PRId64, reading.country, reading.id);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user