mirror of
https://github.com/esphome/esphome.git
synced 2025-11-01 07:31:51 +00:00
Compare commits
93 Commits
2025.10.3
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3163ac85ec | ||
|
|
28454b8219 | ||
|
|
332f52e149 | ||
|
|
ae1f54d398 | ||
|
|
2b3e7f38d2 | ||
|
|
5510ece6ac | ||
|
|
e19a85b523 | ||
|
|
cf02a08209 | ||
|
|
90e8c12df1 | ||
|
|
42bf5840c9 | ||
|
|
47817485e7 | ||
|
|
ded98ff705 | ||
|
|
7f8ca5ddef | ||
|
|
1b0ca3360e | ||
|
|
66263b40e1 | ||
|
|
6d5e41ef7f | ||
|
|
fedfda6c29 | ||
|
|
6e2088f836 | ||
|
|
7a82379c88 | ||
|
|
c983581b6c | ||
|
|
f0ac61f247 | ||
|
|
85420b0606 | ||
|
|
00230f7cc6 | ||
|
|
63a87a5ef3 | ||
|
|
d75ae357c2 | ||
|
|
88d223d03a | ||
|
|
b927b29a0a | ||
|
|
72ec9b672e | ||
|
|
8f49b1da54 | ||
|
|
9ff6f344ab | ||
|
|
0e703ddbba | ||
|
|
2175c2909b | ||
|
|
cbdb9d4a56 | ||
|
|
baa010583e | ||
|
|
8e9a68a107 | ||
|
|
4c688a4b00 | ||
|
|
9eef281895 | ||
|
|
8a15c18066 | ||
|
|
5bb69a968c | ||
|
|
7c02f2f10a | ||
|
|
fe07c34246 | ||
|
|
c652aa375a | ||
|
|
9fb254fdc2 | ||
|
|
3df4dbd3a6 | ||
|
|
6372099df3 | ||
|
|
8d8fcfeda2 | ||
|
|
0f356fcc79 | ||
|
|
aec60d122b | ||
|
|
c10f68ef0c | ||
|
|
bcc424afed | ||
|
|
be2c859df3 | ||
|
|
59f728488e | ||
|
|
04a0de556d | ||
|
|
1f13d44c1b | ||
|
|
9ebfa9aaa8 | ||
|
|
6bc9ed0810 | ||
|
|
9b6e8b4b41 | ||
|
|
cad747c672 | ||
|
|
660adccda3 | ||
|
|
51fbc4f7a3 | ||
|
|
2cc5e24b38 | ||
|
|
3afa73b449 | ||
|
|
dcf2697a2a | ||
|
|
6a11700a6b | ||
|
|
9bd9b043c8 | ||
|
|
cb602c9b1a | ||
|
|
b54beb357a | ||
|
|
6abc2efd96 | ||
|
|
be51093a7e | ||
|
|
52219c4dcc | ||
|
|
590cae13c0 | ||
|
|
e15429b0f5 | ||
|
|
b5cc668a45 | ||
|
|
a1b0ae78e0 | ||
|
|
fcc8a809e6 | ||
|
|
48474c0f8c | ||
|
|
9f9c95dd09 | ||
|
|
a74fcbc8b6 | ||
|
|
c8b898f9c5 | ||
|
|
81bf2688b4 | ||
|
|
87d2c9868f | ||
|
|
5ca407e27c | ||
|
|
5bbc2ab482 | ||
|
|
309e8b4c92 | ||
|
|
eee2987c99 | ||
|
|
061e55f8c5 | ||
|
|
56334b7832 | ||
|
|
a4b7e0c700 | ||
|
|
84ad7ee0e4 | ||
|
|
d006008539 | ||
|
|
6bb1e4c9c0 | ||
|
|
82bdb08884 | ||
|
|
b709ff84c3 |
@@ -221,6 +221,146 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
|
||||
* **Code Generation:** Generate minimal and efficient C++ code. Validate all user inputs thoroughly. Support multiple platform variations.
|
||||
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
|
||||
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
|
||||
|
||||
**STL Container Guidelines:**
|
||||
|
||||
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
|
||||
|
||||
1. **Compile-time-known sizes:** Use `std::array` instead of `std::vector` when size is known at compile time.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code
|
||||
std::vector<int> values;
|
||||
|
||||
// Good - no dynamic allocation
|
||||
std::array<int, MAX_VALUES> values;
|
||||
```
|
||||
Use `cg.add_define("MAX_VALUES", count)` to set the size from Python configuration.
|
||||
|
||||
**For byte buffers:** Avoid `std::vector<uint8_t>` unless the buffer needs to grow. Use `std::unique_ptr<uint8_t[]>` instead.
|
||||
|
||||
> **Note:** `std::unique_ptr<uint8_t[]>` does **not** provide bounds checking or iterator support like `std::vector<uint8_t>`. Use it only when you do not need these features and want minimal overhead.
|
||||
|
||||
```cpp
|
||||
// Bad - STL overhead for simple byte buffer
|
||||
std::vector<uint8_t> buffer;
|
||||
buffer.resize(256);
|
||||
|
||||
// Good - minimal overhead, single allocation
|
||||
std::unique_ptr<uint8_t[]> buffer = std::make_unique<uint8_t[]>(256);
|
||||
// Or if size is constant:
|
||||
std::array<uint8_t, 256> buffer;
|
||||
```
|
||||
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<ServiceRecord> services;
|
||||
services.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - compile-time fixed size, stack allocated, no reallocation machinery
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
|
||||
services.push_back(record1); // Tracks count but all slots allocated
|
||||
```
|
||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||
|
||||
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<TxtRecord> txt_records;
|
||||
txt_records.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - runtime size, single allocation, no reallocation machinery
|
||||
FixedVector<TxtRecord> txt_records;
|
||||
txt_records.init(record_count); // Initialize with exact size at runtime
|
||||
```
|
||||
**Benefits:**
|
||||
- Eliminates `_M_realloc_insert`, `_M_default_append` template instantiations (saves 200-500 bytes per instance)
|
||||
- Single allocation, no upper bound needed
|
||||
- No reallocation overhead
|
||||
- Compatible with protobuf code generation when using `[(fixed_vector) = true]` option
|
||||
|
||||
4. **Small datasets (1-16 elements):** Use `std::vector` or `std::array` with simple structs instead of `std::map`/`std::set`/`std::unordered_map`.
|
||||
```cpp
|
||||
// Bad - 2KB+ overhead for red-black tree/hash table
|
||||
std::map<std::string, int> small_lookup;
|
||||
std::unordered_map<int, std::string> tiny_map;
|
||||
|
||||
// Good - simple struct with linear search (std::vector is fine)
|
||||
struct LookupEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
};
|
||||
std::vector<LookupEntry> small_lookup = {
|
||||
{"key1", 10},
|
||||
{"key2", 20},
|
||||
{"key3", 30},
|
||||
};
|
||||
// Or std::array if size is compile-time constant:
|
||||
// std::array<LookupEntry, 3> small_lookup = {{ ... }};
|
||||
```
|
||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||
|
||||
5. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
- Red-black tree code (`rb_tree`, `_Rb_tree`)
|
||||
- Hash table infrastructure (`unordered_map`, `hash`)
|
||||
|
||||
**When to optimize:**
|
||||
- Core components (API, network, logger)
|
||||
- Widely-used components (mdns, wifi, ble)
|
||||
- Components causing flash size complaints
|
||||
|
||||
**When not to optimize:**
|
||||
- Single-use niche components
|
||||
- Code where readability matters more than bytes
|
||||
- Already using appropriate containers
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
**Bad Pattern (Module-Level Globals):**
|
||||
```python
|
||||
# Don't do this - state persists between compilation runs
|
||||
_component_state = []
|
||||
_use_feature = None
|
||||
|
||||
def enable_feature():
|
||||
global _use_feature
|
||||
_use_feature = True
|
||||
```
|
||||
|
||||
**Good Pattern (CORE.data with Helpers):**
|
||||
```python
|
||||
from esphome.core import CORE
|
||||
|
||||
# Keys for CORE.data storage
|
||||
COMPONENT_STATE_KEY = "my_component_state"
|
||||
USE_FEATURE_KEY = "my_component_use_feature"
|
||||
|
||||
def _get_component_state() -> list:
|
||||
"""Get component state from CORE.data."""
|
||||
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
|
||||
|
||||
def _get_use_feature() -> bool | None:
|
||||
"""Get feature flag from CORE.data."""
|
||||
return CORE.data.get(USE_FEATURE_KEY)
|
||||
|
||||
def _set_use_feature(value: bool) -> None:
|
||||
"""Set feature flag in CORE.data."""
|
||||
CORE.data[USE_FEATURE_KEY] = value
|
||||
|
||||
def enable_feature():
|
||||
_set_use_feature(True)
|
||||
```
|
||||
|
||||
**Why this matters:**
|
||||
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
||||
- `CORE.data` automatically clears between runs
|
||||
- Typed helper functions provide better IDE support and maintainability
|
||||
- Encapsulation makes state management explicit and testable
|
||||
|
||||
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
|
||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
|
||||
|
||||
82
.github/workflows/ci.yml
vendored
82
.github/workflows/ci.yml
vendored
@@ -114,8 +114,7 @@ jobs:
|
||||
matrix:
|
||||
python-version:
|
||||
- "3.11"
|
||||
- "3.12"
|
||||
- "3.13"
|
||||
- "3.14"
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macOS-latest
|
||||
@@ -124,13 +123,9 @@ jobs:
|
||||
# Minimize CI resource usage
|
||||
# by only running the Python version
|
||||
# version used for docker images on Windows and macOS
|
||||
- python-version: "3.13"
|
||||
- python-version: "3.14"
|
||||
os: windows-latest
|
||||
- python-version: "3.12"
|
||||
os: windows-latest
|
||||
- python-version: "3.13"
|
||||
os: macOS-latest
|
||||
- python-version: "3.12"
|
||||
- python-version: "3.14"
|
||||
os: macOS-latest
|
||||
runs-on: ${{ matrix.os }}
|
||||
needs:
|
||||
@@ -178,6 +173,7 @@ jobs:
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
changed-components: ${{ steps.determine.outputs.changed-components }}
|
||||
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
|
||||
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
|
||||
component-test-count: ${{ steps.determine.outputs.component-test-count }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -206,6 +202,7 @@ jobs:
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
|
||||
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
|
||||
|
||||
integration-tests:
|
||||
@@ -358,48 +355,13 @@ jobs:
|
||||
# yamllint disable-line rule:line-length
|
||||
if: always()
|
||||
|
||||
test-build-components:
|
||||
name: Component test ${{ matrix.file }}
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 && fromJSON(needs.determine-jobs.outputs.component-test-count) < 100
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: 2
|
||||
matrix:
|
||||
file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
|
||||
steps:
|
||||
- name: Cache apt packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
|
||||
with:
|
||||
packages: libsdl2-dev
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Validate config for ${{ matrix.file }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python3 script/test_build_components.py -e config -c ${{ matrix.file }}
|
||||
- name: Compile config for ${{ matrix.file }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
|
||||
|
||||
test-build-components-splitter:
|
||||
name: Split components for intelligent grouping (40 weighted per batch)
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||
outputs:
|
||||
matrix: ${{ steps.split.outputs.components }}
|
||||
steps:
|
||||
@@ -417,9 +379,10 @@ jobs:
|
||||
|
||||
# Use intelligent splitter that groups components with same bus configs
|
||||
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
|
||||
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
|
||||
|
||||
echo "Splitting components intelligently..."
|
||||
output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
|
||||
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
|
||||
|
||||
echo "$output" >> $GITHUB_OUTPUT
|
||||
|
||||
@@ -430,7 +393,7 @@ jobs:
|
||||
- common
|
||||
- determine-jobs
|
||||
- test-build-components-splitter
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
|
||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
|
||||
@@ -477,18 +440,34 @@ jobs:
|
||||
# Convert space-separated components to comma-separated for Python script
|
||||
components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
|
||||
|
||||
echo "Testing components: $components_csv"
|
||||
# Only isolate directly changed components when targeting dev branch
|
||||
# For beta/release branches, group everything for faster CI
|
||||
#
|
||||
# WHY ISOLATE DIRECTLY CHANGED COMPONENTS?
|
||||
# - Isolated tests run WITHOUT --testing-mode, enabling full validation
|
||||
# - This catches pin conflicts and other issues in directly changed code
|
||||
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
|
||||
# - Dependencies are safe to group since they weren't modified in this PR
|
||||
if [ "${{ github.base_ref }}" = "beta" ] || [ "${{ github.base_ref }}" = "release" ]; then
|
||||
directly_changed_csv=""
|
||||
echo "Testing components: $components_csv"
|
||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||
else
|
||||
directly_changed_csv=$(echo '${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | jq -r 'join(",")')
|
||||
echo "Testing components: $components_csv"
|
||||
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components: $directly_changed_csv"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# Run config validation with grouping
|
||||
python3 script/test_build_components.py -e config -c "$components_csv" -f
|
||||
# Run config validation with grouping and isolation
|
||||
python3 script/test_build_components.py -e config -c "$components_csv" -f --isolate "$directly_changed_csv"
|
||||
|
||||
echo ""
|
||||
echo "Config validation passed! Starting compilation..."
|
||||
echo ""
|
||||
|
||||
# Run compilation with grouping
|
||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f
|
||||
# Run compilation with grouping and isolation
|
||||
python3 script/test_build_components.py -e compile -c "$components_csv" -f --isolate "$directly_changed_csv"
|
||||
|
||||
pre-commit-ci-lite:
|
||||
name: pre-commit.ci lite
|
||||
@@ -521,7 +500,6 @@ jobs:
|
||||
- integration-tests
|
||||
- clang-tidy
|
||||
- determine-jobs
|
||||
- test-build-components
|
||||
- test-build-components-splitter
|
||||
- test-build-components-split
|
||||
- pre-commit-ci-lite
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
|
||||
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
operations-per-run: 150
|
||||
operations-per-run: 400
|
||||
|
||||
# The 90 day stale policy for PRs
|
||||
# - PRs
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.10.0
|
||||
PROJECT_NUMBER = 2025.11.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -776,9 +776,9 @@ message HomeassistantActionRequest {
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2;
|
||||
repeated HomeassistantServiceMap data_template = 3;
|
||||
repeated HomeassistantServiceMap variables = 4;
|
||||
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
|
||||
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
|
||||
bool is_event = 5;
|
||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
@@ -866,7 +866,7 @@ message ListEntitiesServicesResponse {
|
||||
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3;
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
@@ -1519,7 +1519,7 @@ message BluetoothGATTCharacteristic {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
uint32 properties = 3;
|
||||
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||
repeated BluetoothGATTDescriptor descriptors = 4 [(fixed_vector) = true];
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
@@ -1531,7 +1531,7 @@ message BluetoothGATTCharacteristic {
|
||||
message BluetoothGATTService {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||
repeated BluetoothGATTCharacteristic characteristics = 3 [(fixed_vector) = true];
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
|
||||
@@ -64,4 +64,10 @@ extend google.protobuf.FieldOptions {
|
||||
// This is typically done through methods returning const T& or special accessor
|
||||
// methods like get_options() or supported_modes_for_api_().
|
||||
optional string container_pointer = 50001;
|
||||
|
||||
// fixed_vector: Use FixedVector instead of std::vector for repeated fields
|
||||
// When set, the repeated field will use FixedVector<T> which requires calling
|
||||
// init(size) before adding elements. This eliminates std::vector template overhead
|
||||
// and is ideal when the exact size is known before populating the array.
|
||||
optional bool fixed_vector = 50013 [default=false];
|
||||
}
|
||||
|
||||
@@ -1110,9 +1110,9 @@ class HomeassistantActionRequest final : public ProtoMessage {
|
||||
#endif
|
||||
StringRef service_ref_{};
|
||||
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
|
||||
std::vector<HomeassistantServiceMap> data{};
|
||||
std::vector<HomeassistantServiceMap> data_template{};
|
||||
std::vector<HomeassistantServiceMap> variables{};
|
||||
FixedVector<HomeassistantServiceMap> data{};
|
||||
FixedVector<HomeassistantServiceMap> data_template{};
|
||||
FixedVector<HomeassistantServiceMap> variables{};
|
||||
bool is_event{false};
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
uint32_t call_id{0};
|
||||
@@ -1263,7 +1263,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
std::vector<ListEntitiesServicesArgument> args{};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1923,7 +1923,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage {
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
uint32_t properties{0};
|
||||
std::vector<BluetoothGATTDescriptor> descriptors{};
|
||||
FixedVector<BluetoothGATTDescriptor> descriptors{};
|
||||
uint32_t short_uuid{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
@@ -1937,7 +1937,7 @@ class BluetoothGATTService final : public ProtoMessage {
|
||||
public:
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
std::vector<BluetoothGATTCharacteristic> characteristics{};
|
||||
FixedVector<BluetoothGATTCharacteristic> characteristics{};
|
||||
uint32_t short_uuid{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
|
||||
@@ -201,9 +201,9 @@ class CustomAPIDevice {
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
}
|
||||
@@ -244,9 +244,9 @@ class CustomAPIDevice {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
}
|
||||
|
||||
@@ -127,24 +127,9 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
resp.is_event = this->flags_.is_event;
|
||||
for (auto &it : this->data_) {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->data_template_) {
|
||||
resp.data_template.emplace_back();
|
||||
auto &kv = resp.data_template.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->variables_) {
|
||||
resp.variables.emplace_back();
|
||||
auto &kv = resp.variables.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
this->populate_service_map(resp.data, this->data_, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, x...);
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
if (this->flags_.wants_status) {
|
||||
@@ -189,6 +174,16 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
protected:
|
||||
template<typename VectorType, typename SourceType>
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
||||
dest.init(source.size());
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
TemplatableStringValue<Ts...> service_{};
|
||||
std::vector<TemplatableKeyValuePair<Ts...>> data_;
|
||||
|
||||
@@ -749,13 +749,29 @@ class ProtoSize {
|
||||
template<typename MessageType>
|
||||
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
if (!messages.empty()) {
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_force(field_id_size, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_force(field_id_size, message);
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
|
||||
* version)
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the FixedVector
|
||||
* @param messages FixedVector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
|
||||
// Skip if the fixed vector is empty
|
||||
if (!messages.empty()) {
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_force(field_id_size, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -35,9 +35,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
msg.args.emplace_back();
|
||||
auto &arg = msg.args.back();
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
}
|
||||
|
||||
@@ -230,8 +230,8 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
if (total_char_count > 0) {
|
||||
// Reserve space and process characteristics
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
// Initialize FixedVector with exact count and process characteristics
|
||||
service_resp.characteristics.init(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
@@ -253,9 +253,7 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
|
||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
||||
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
@@ -271,12 +269,11 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
// No descriptors, continue to next characteristic
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserve space and process descriptors
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
// Initialize FixedVector with exact count and process descriptors
|
||||
characteristic_resp.descriptors.init(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
@@ -297,9 +294,7 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
|
||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
||||
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
#include "bluetooth_connection.h"
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_bt_device.h>
|
||||
|
||||
namespace esphome::bluetooth_proxy {
|
||||
|
||||
@@ -775,7 +775,7 @@ void Display::test_card() {
|
||||
int shift_y = (h - image_h) / 2;
|
||||
int line_w = (image_w - 6) / 6;
|
||||
int image_c = image_w / 2;
|
||||
for (auto i = 0; i <= image_h; i++) {
|
||||
for (auto i = 0; i != image_h; i++) {
|
||||
int c = esp_scale(i, image_h);
|
||||
this->horizontal_line(shift_x + 0, shift_y + i, line_w, r.fade_to_white(c));
|
||||
this->horizontal_line(shift_x + line_w, shift_y + i, line_w, r.fade_to_black(c)); //
|
||||
@@ -809,8 +809,11 @@ void Display::test_card() {
|
||||
}
|
||||
}
|
||||
}
|
||||
this->rectangle(0, 0, w, h, Color(127, 0, 127));
|
||||
this->filled_rectangle(0, 0, 10, 10, Color(255, 0, 255));
|
||||
this->filled_rectangle(w - 10, 0, 10, 10, Color(255, 0, 255));
|
||||
this->filled_rectangle(0, h - 10, 10, 10, Color(255, 0, 255));
|
||||
this->filled_rectangle(w - 10, h - 10, 10, 10, Color(255, 0, 255));
|
||||
this->rectangle(0, 0, w, h, Color(255, 255, 255));
|
||||
this->stop_poller();
|
||||
}
|
||||
|
||||
|
||||
@@ -304,6 +304,17 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
|
||||
|
||||
|
||||
def _is_framework_url(source: str) -> str:
|
||||
# platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
|
||||
import urllib.parse
|
||||
|
||||
try:
|
||||
parsed = urllib.parse.urlparse(source)
|
||||
except ValueError:
|
||||
return False
|
||||
return bool(parsed.scheme)
|
||||
|
||||
|
||||
# NOTE: Keep this in mind when updating the recommended version:
|
||||
# * New framework historically have had some regressions, especially for WiFi.
|
||||
# The new version needs to be thoroughly validated before changing the
|
||||
@@ -313,7 +324,7 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||
# The default/recommended arduino framework version
|
||||
# - https://github.com/espressif/arduino-esp32/releases
|
||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(3, 2, 1),
|
||||
"recommended": cv.Version(3, 3, 2),
|
||||
"latest": cv.Version(3, 3, 2),
|
||||
"dev": cv.Version(3, 3, 2),
|
||||
}
|
||||
@@ -332,7 +343,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(5, 4, 2),
|
||||
"recommended": cv.Version(5, 5, 1),
|
||||
"latest": cv.Version(5, 5, 1),
|
||||
"dev": cv.Version(5, 5, 1),
|
||||
}
|
||||
@@ -352,7 +363,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||
# The platform-espressif32 version
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
PLATFORM_VERSION_LOOKUP = {
|
||||
"recommended": cv.Version(54, 3, 21, "2"),
|
||||
"recommended": cv.Version(55, 3, 31, "1"),
|
||||
"latest": cv.Version(55, 3, 31, "1"),
|
||||
"dev": cv.Version(55, 3, 31, "1"),
|
||||
}
|
||||
@@ -387,7 +398,7 @@ def _check_versions(value):
|
||||
value[CONF_SOURCE] = value.get(
|
||||
CONF_SOURCE, _format_framework_arduino_version(version)
|
||||
)
|
||||
if value[CONF_SOURCE].startswith("http"):
|
||||
if _is_framework_url(value[CONF_SOURCE]):
|
||||
value[CONF_SOURCE] = (
|
||||
f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
|
||||
)
|
||||
@@ -400,7 +411,7 @@ def _check_versions(value):
|
||||
CONF_SOURCE,
|
||||
_format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
|
||||
)
|
||||
if value[CONF_SOURCE].startswith("http"):
|
||||
if _is_framework_url(value[CONF_SOURCE]):
|
||||
value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
|
||||
|
||||
if CONF_PLATFORM_VERSION not in value:
|
||||
@@ -533,6 +544,7 @@ CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
|
||||
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
|
||||
|
||||
|
||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||
@@ -595,6 +607,9 @@ FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||
}
|
||||
),
|
||||
@@ -853,6 +868,12 @@ async def to_code(config):
|
||||
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
|
||||
|
||||
# Disable placing libc locks in IRAM to save RAM
|
||||
# This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled)
|
||||
# use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM.
|
||||
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
|
||||
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
add_extra_build_file(
|
||||
|
||||
@@ -1564,6 +1564,10 @@ BOARDS = {
|
||||
"name": "DFRobot Beetle ESP32-C3",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"dfrobot_firebeetle2_esp32c6": {
|
||||
"name": "DFRobot FireBeetle 2 ESP32-C6",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
},
|
||||
"dfrobot_firebeetle2_esp32e": {
|
||||
"name": "DFRobot Firebeetle 2 ESP32-E",
|
||||
"variant": VARIANT_ESP32,
|
||||
@@ -1604,6 +1608,22 @@ BOARDS = {
|
||||
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
|
||||
"variant": VARIANT_ESP32C3,
|
||||
},
|
||||
"esp32-c5-devkitc-1": {
|
||||
"name": "Espressif ESP32-C5-DevKitC-1 4MB no PSRAM",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"esp32-c5-devkitc1-n16r4": {
|
||||
"name": "Espressif ESP32-C5-DevKitC-1 N16R4 (16 MB Flash Quad, 4 MB PSRAM Quad)",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"esp32-c5-devkitc1-n4": {
|
||||
"name": "Espressif ESP32-C5-DevKitC-1 N4 (4MB no PSRAM)",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"esp32-c5-devkitc1-n8r4": {
|
||||
"name": "Espressif ESP32-C5-DevKitC-1 N8R4 (8 MB Flash Quad, 4 MB PSRAM Quad)",
|
||||
"variant": VARIANT_ESP32C5,
|
||||
},
|
||||
"esp32-c6-devkitc-1": {
|
||||
"name": "Espressif ESP32-C6-DevKitC-1",
|
||||
"variant": VARIANT_ESP32C6,
|
||||
@@ -2048,6 +2068,10 @@ BOARDS = {
|
||||
"name": "M5Stack Station",
|
||||
"variant": VARIANT_ESP32,
|
||||
},
|
||||
"m5stack-tab5-p4": {
|
||||
"name": "M5STACK Tab5 esp32-p4 Board",
|
||||
"variant": VARIANT_ESP32P4,
|
||||
},
|
||||
"m5stack-timer-cam": {
|
||||
"name": "M5Stack Timer CAM",
|
||||
"variant": VARIANT_ESP32,
|
||||
@@ -2476,6 +2500,10 @@ BOARDS = {
|
||||
"name": "YelloByte YB-ESP32-S3-AMP (Rev.3)",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_drv": {
|
||||
"name": "YelloByte YB-ESP32-S3-DRV",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
},
|
||||
"yb_esp32s3_eth": {
|
||||
"name": "YelloByte YB-ESP32-S3-ETH",
|
||||
"variant": VARIANT_ESP32S3,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from collections.abc import Callable, MutableMapping
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
import logging
|
||||
import re
|
||||
@@ -16,7 +17,7 @@ from esphome.const import (
|
||||
CONF_NAME,
|
||||
CONF_NAME_ADD_MAC_SUFFIX,
|
||||
)
|
||||
from esphome.core import CORE, TimePeriod
|
||||
from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
@@ -111,6 +112,58 @@ class BTLoggers(Enum):
|
||||
_required_loggers: set[BTLoggers] = set()
|
||||
|
||||
|
||||
# Dataclass for handler registration counts
|
||||
@dataclass
|
||||
class HandlerCounts:
|
||||
gap_event: int = 0
|
||||
gap_scan_event: int = 0
|
||||
gattc_event: int = 0
|
||||
gatts_event: int = 0
|
||||
ble_status_event: int = 0
|
||||
|
||||
|
||||
# Track handler registration counts for StaticVector sizing
|
||||
_handler_counts = HandlerCounts()
|
||||
|
||||
|
||||
def register_gap_event_handler(parent_var: cg.MockObj, handler_var: cg.MockObj) -> None:
|
||||
"""Register a GAP event handler and track the count."""
|
||||
_handler_counts.gap_event += 1
|
||||
cg.add(parent_var.register_gap_event_handler(handler_var))
|
||||
|
||||
|
||||
def register_gap_scan_event_handler(
|
||||
parent_var: cg.MockObj, handler_var: cg.MockObj
|
||||
) -> None:
|
||||
"""Register a GAP scan event handler and track the count."""
|
||||
_handler_counts.gap_scan_event += 1
|
||||
cg.add(parent_var.register_gap_scan_event_handler(handler_var))
|
||||
|
||||
|
||||
def register_gattc_event_handler(
|
||||
parent_var: cg.MockObj, handler_var: cg.MockObj
|
||||
) -> None:
|
||||
"""Register a GATTc event handler and track the count."""
|
||||
_handler_counts.gattc_event += 1
|
||||
cg.add(parent_var.register_gattc_event_handler(handler_var))
|
||||
|
||||
|
||||
def register_gatts_event_handler(
|
||||
parent_var: cg.MockObj, handler_var: cg.MockObj
|
||||
) -> None:
|
||||
"""Register a GATTs event handler and track the count."""
|
||||
_handler_counts.gatts_event += 1
|
||||
cg.add(parent_var.register_gatts_event_handler(handler_var))
|
||||
|
||||
|
||||
def register_ble_status_event_handler(
|
||||
parent_var: cg.MockObj, handler_var: cg.MockObj
|
||||
) -> None:
|
||||
"""Register a BLE status event handler and track the count."""
|
||||
_handler_counts.ble_status_event += 1
|
||||
cg.add(parent_var.register_ble_status_event_handler(handler_var))
|
||||
|
||||
|
||||
def register_bt_logger(*loggers: BTLoggers) -> None:
|
||||
"""Register Bluetooth logger categories that a component needs.
|
||||
|
||||
@@ -334,6 +387,15 @@ def final_validation(config):
|
||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||
validate_connection_slots(max_connections)
|
||||
|
||||
# Check if hosted bluetooth is being used
|
||||
if "esp32_hosted" in full_config:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_CLASSIC_ENABLED", False)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLUEDROID_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_CONTROLLER_DISABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID", True)
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_BLUEDROID_HCI_VHCI", True)
|
||||
|
||||
# Check if BLE Server is needed
|
||||
has_ble_server = "esp32_ble_server" in full_config
|
||||
|
||||
@@ -374,6 +436,36 @@ def final_validation(config):
|
||||
FINAL_VALIDATE_SCHEMA = final_validation
|
||||
|
||||
|
||||
# This needs to be run as a job with CoroPriority.FINAL priority so that all components have
|
||||
# a chance to register their handlers before the counts are added to defines.
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def _add_ble_handler_defines():
|
||||
# Add defines for StaticVector sizing based on handler registration counts
|
||||
# Only define if count > 0 to avoid allocating unnecessary memory
|
||||
if _handler_counts.gap_event > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT", _handler_counts.gap_event
|
||||
)
|
||||
if _handler_counts.gap_scan_event > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT",
|
||||
_handler_counts.gap_scan_event,
|
||||
)
|
||||
if _handler_counts.gattc_event > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT", _handler_counts.gattc_event
|
||||
)
|
||||
if _handler_counts.gatts_event > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT", _handler_counts.gatts_event
|
||||
)
|
||||
if _handler_counts.ble_status_event > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT",
|
||||
_handler_counts.ble_status_event,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
|
||||
@@ -428,6 +520,9 @@ async def to_code(config):
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
cg.add_define("USE_ESP32_BLE_UUID")
|
||||
|
||||
# Schedule the handler defines to be added after all components register
|
||||
CORE.add_job(_add_ble_handler_defines)
|
||||
|
||||
|
||||
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
|
||||
async def ble_enabled_to_code(config, condition_id, template_arg, args):
|
||||
|
||||
@@ -6,7 +6,15 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#else
|
||||
extern "C" {
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_misc.h>
|
||||
#include <esp_hosted_bluedroid.h>
|
||||
}
|
||||
#endif
|
||||
#include <esp_bt_device.h>
|
||||
#include <esp_bt_main.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
@@ -136,6 +144,7 @@ void ESP32BLE::advertising_init_() {
|
||||
|
||||
bool ESP32BLE::ble_setup_() {
|
||||
esp_err_t err;
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#ifdef USE_ARDUINO
|
||||
if (!btStart()) {
|
||||
ESP_LOGE(TAG, "btStart failed: %d", esp_bt_controller_get_status());
|
||||
@@ -169,6 +178,28 @@ bool ESP32BLE::ble_setup_() {
|
||||
#endif
|
||||
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
||||
#else
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
|
||||
if (esp_hosted_bt_controller_init() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_enable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
hosted_hci_bluedroid_open();
|
||||
|
||||
esp_bluedroid_hci_driver_operations_t operations = {
|
||||
.send = hosted_hci_bluedroid_send,
|
||||
.check_send_available = hosted_hci_bluedroid_check_send_available,
|
||||
.register_host_callback = hosted_hci_bluedroid_register_host_callback,
|
||||
};
|
||||
esp_bluedroid_attach_hci_driver(&operations);
|
||||
#endif
|
||||
|
||||
err = esp_bluedroid_init();
|
||||
if (err != ESP_OK) {
|
||||
@@ -181,31 +212,27 @@ bool ESP32BLE::ble_setup_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->gap_event_handlers_.empty()) {
|
||||
err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_SERVER
|
||||
if (!this->gatts_event_handlers_.empty()) {
|
||||
err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
err = esp_ble_gap_register_callback(ESP32BLE::gap_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gap_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_BLE_CLIENT
|
||||
if (!this->gattc_event_handlers_.empty()) {
|
||||
err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
|
||||
err = esp_ble_gatts_register_callback(ESP32BLE::gatts_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gatts_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
|
||||
err = esp_ble_gattc_register_callback(ESP32BLE::gattc_event_handler);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_ble_gattc_register_callback failed: %d", err);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -213,8 +240,11 @@ bool ESP32BLE::ble_setup_() {
|
||||
if (this->name_.has_value()) {
|
||||
name = this->name_.value();
|
||||
if (App.is_name_add_mac_suffix_enabled()) {
|
||||
name += "-";
|
||||
name += get_mac_address().substr(6);
|
||||
// MAC address suffix length (last 6 characters of 12-char MAC address string)
|
||||
constexpr size_t mac_address_suffix_len = 6;
|
||||
const std::string mac_addr = get_mac_address();
|
||||
const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len;
|
||||
name = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len);
|
||||
}
|
||||
} else {
|
||||
name = App.get_name();
|
||||
@@ -258,6 +288,7 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#ifdef USE_ARDUINO
|
||||
if (!btStop()) {
|
||||
ESP_LOGE(TAG, "btStop failed: %d", esp_bt_controller_get_status());
|
||||
@@ -287,6 +318,19 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
if (esp_hosted_bt_controller_disable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
hosted_hci_bluedroid_close();
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
@@ -299,9 +343,11 @@ void ESP32BLE::loop() {
|
||||
case BLE_COMPONENT_STATE_DISABLE: {
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
for (auto *ble_event_handler : this->ble_status_event_handlers_) {
|
||||
ble_event_handler->ble_before_disabled_event_handler();
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!ble_dismantle_()) {
|
||||
ESP_LOGE(TAG, "Could not be dismantled");
|
||||
@@ -331,7 +377,7 @@ void ESP32BLE::loop() {
|
||||
BLEEvent *ble_event = this->ble_events_.pop();
|
||||
while (ble_event != nullptr) {
|
||||
switch (ble_event->type_) {
|
||||
#ifdef USE_ESP32_BLE_SERVER
|
||||
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
|
||||
case BLEEvent::GATTS: {
|
||||
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
|
||||
esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
|
||||
@@ -343,7 +389,7 @@ void ESP32BLE::loop() {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ESP32_BLE_CLIENT
|
||||
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
|
||||
case BLEEvent::GATTC: {
|
||||
esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
|
||||
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
|
||||
@@ -359,10 +405,12 @@ void ESP32BLE::loop() {
|
||||
esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
|
||||
switch (gap_event) {
|
||||
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
|
||||
// Use the new scan event handler - no memcpy!
|
||||
for (auto *scan_handler : this->gap_scan_event_handlers_) {
|
||||
scan_handler->gap_scan_event_handler(ble_event->scan_result());
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Scan complete events
|
||||
@@ -374,10 +422,12 @@ void ESP32BLE::loop() {
|
||||
// This is verified at compile-time by static_assert checks in ble_event.h
|
||||
// The struct already contains our copy of the status (copied in BLEEvent constructor)
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Advertising complete events
|
||||
@@ -388,19 +438,23 @@ void ESP32BLE::loop() {
|
||||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
||||
// All advertising complete events have the same structure with just status
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
// RSSI complete event
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
// Security events
|
||||
@@ -410,10 +464,12 @@ void ESP32BLE::loop() {
|
||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
|
||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||
gap_handler->gap_event_handler(
|
||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
|
||||
}
|
||||
#endif
|
||||
break;
|
||||
|
||||
default:
|
||||
|
||||
@@ -125,19 +125,25 @@ class ESP32BLE : public Component {
|
||||
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
|
||||
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
|
||||
this->gap_scan_event_handlers_.push_back(handler);
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_CLIENT
|
||||
#endif
|
||||
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
|
||||
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
|
||||
#endif
|
||||
#ifdef USE_ESP32_BLE_SERVER
|
||||
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
|
||||
void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
|
||||
this->ble_status_event_handlers_.push_back(handler);
|
||||
}
|
||||
#endif
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
|
||||
protected:
|
||||
@@ -159,16 +165,22 @@ class ESP32BLE : public Component {
|
||||
private:
|
||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||
|
||||
// Vectors (12 bytes each on 32-bit, naturally aligned to 4 bytes)
|
||||
std::vector<GAPEventHandler *> gap_event_handlers_;
|
||||
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
|
||||
#ifdef USE_ESP32_BLE_CLIENT
|
||||
std::vector<GATTcEventHandler *> gattc_event_handlers_;
|
||||
// Handler vectors - use StaticVector when counts are known at compile time
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||
StaticVector<GAPEventHandler *, ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT> gap_event_handlers_;
|
||||
#endif
|
||||
#ifdef USE_ESP32_BLE_SERVER
|
||||
std::vector<GATTsEventHandler *> gatts_event_handlers_;
|
||||
#ifdef ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT
|
||||
StaticVector<GAPScanEventHandler *, ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT> gap_scan_event_handlers_;
|
||||
#endif
|
||||
#if defined(USE_ESP32_BLE_CLIENT) && defined(ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT)
|
||||
StaticVector<GATTcEventHandler *, ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT> gattc_event_handlers_;
|
||||
#endif
|
||||
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
|
||||
StaticVector<GATTsEventHandler *, ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT> gatts_event_handlers_;
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT
|
||||
StaticVector<BLEStatusEventHandler *, ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT> ble_status_event_handlers_;
|
||||
#endif
|
||||
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
|
||||
|
||||
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
|
||||
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID], uuid_arr)
|
||||
|
||||
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
|
||||
cg.add(parent.register_gap_event_handler(var))
|
||||
esp32_ble.register_gap_event_handler(parent, var)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
cg.add(var.set_major(config[CONF_MAJOR]))
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_bt_main.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_gap_ble_api.h>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -546,8 +546,8 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
|
||||
cg.add(parent.register_gatts_event_handler(var))
|
||||
cg.add(parent.register_ble_status_event_handler(var))
|
||||
esp32_ble.register_gatts_event_handler(parent, var)
|
||||
esp32_ble.register_ble_status_event_handler(parent, var)
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
|
||||
if CONF_MANUFACTURER_DATA in config:
|
||||
|
||||
@@ -10,7 +10,9 @@
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <freertos/task.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
|
||||
from esphome import automation
|
||||
@@ -52,9 +53,19 @@ class BLEFeatures(StrEnum):
|
||||
ESP_BT_DEVICE = "ESP_BT_DEVICE"
|
||||
|
||||
|
||||
# Dataclass for registration counts
|
||||
@dataclass
|
||||
class RegistrationCounts:
|
||||
listeners: int = 0
|
||||
clients: int = 0
|
||||
|
||||
|
||||
# Set to track which features are needed by components
|
||||
_required_features: set[BLEFeatures] = set()
|
||||
|
||||
# Track registration counts for StaticVector sizing
|
||||
_registration_counts = RegistrationCounts()
|
||||
|
||||
|
||||
def register_ble_features(features: set[BLEFeatures]) -> None:
|
||||
"""Register BLE features that a component needs.
|
||||
@@ -235,10 +246,10 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
|
||||
cg.add(parent.register_gap_event_handler(var))
|
||||
cg.add(parent.register_gap_scan_event_handler(var))
|
||||
cg.add(parent.register_gattc_event_handler(var))
|
||||
cg.add(parent.register_ble_status_event_handler(var))
|
||||
esp32_ble.register_gap_event_handler(parent, var)
|
||||
esp32_ble.register_gap_scan_event_handler(parent, var)
|
||||
esp32_ble.register_gattc_event_handler(parent, var)
|
||||
esp32_ble.register_ble_status_event_handler(parent, var)
|
||||
cg.add(var.set_parent(parent))
|
||||
|
||||
params = config[CONF_SCAN_PARAMETERS]
|
||||
@@ -257,12 +268,14 @@ async def to_code(config):
|
||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||
|
||||
for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
|
||||
_registration_counts.listeners += 1
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
if CONF_MAC_ADDRESS in conf:
|
||||
addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
|
||||
cg.add(trigger.set_addresses(addr_list))
|
||||
await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
|
||||
for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
|
||||
_registration_counts.listeners += 1
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
|
||||
cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
|
||||
@@ -275,6 +288,7 @@ async def to_code(config):
|
||||
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
||||
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
||||
for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
|
||||
_registration_counts.listeners += 1
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
|
||||
cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
|
||||
@@ -287,6 +301,7 @@ async def to_code(config):
|
||||
cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
|
||||
await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
|
||||
for conf in config.get(CONF_ON_SCAN_END, []):
|
||||
_registration_counts.listeners += 1
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
@@ -320,6 +335,17 @@ async def _add_ble_features():
|
||||
cg.add_define("USE_ESP32_BLE_DEVICE")
|
||||
cg.add_define("USE_ESP32_BLE_UUID")
|
||||
|
||||
# Add defines for StaticVector sizing based on registration counts
|
||||
# Only define if count > 0 to avoid allocating unnecessary memory
|
||||
if _registration_counts.listeners > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
|
||||
)
|
||||
if _registration_counts.clients > 0:
|
||||
cg.add_define(
|
||||
"ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
|
||||
)
|
||||
|
||||
|
||||
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -369,6 +395,7 @@ async def register_ble_device(
|
||||
var: cg.SafeExpType, config: ConfigType
|
||||
) -> cg.SafeExpType:
|
||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||
_registration_counts.listeners += 1
|
||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_listener(var))
|
||||
return var
|
||||
@@ -376,6 +403,7 @@ async def register_ble_device(
|
||||
|
||||
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
|
||||
register_ble_features({BLEFeatures.ESP_BT_DEVICE})
|
||||
_registration_counts.clients += 1
|
||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_client(var))
|
||||
return var
|
||||
@@ -389,6 +417,7 @@ async def register_raw_ble_device(
|
||||
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
||||
will not be compiled in if this is the only registration method used.
|
||||
"""
|
||||
_registration_counts.listeners += 1
|
||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_listener(var))
|
||||
return var
|
||||
@@ -402,6 +431,7 @@ async def register_raw_client(
|
||||
This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
|
||||
will not be compiled in if this is the only registration method used.
|
||||
"""
|
||||
_registration_counts.clients += 1
|
||||
paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
|
||||
cg.add(paren.register_client(var))
|
||||
return var
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#endif
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_bt_main.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
@@ -74,9 +76,11 @@ void ESP32BLETracker::setup() {
|
||||
[this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
|
||||
if (state == ota::OTA_STARTED) {
|
||||
this->stop_scan();
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->disconnect();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
});
|
||||
#endif
|
||||
@@ -206,8 +210,10 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
this->set_scanner_state_(ScannerState::STARTING);
|
||||
ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
|
||||
if (!first) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
this->already_discovered_.clear();
|
||||
@@ -236,20 +242,25 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
}
|
||||
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
client->app_id = ++this->app_id_;
|
||||
this->clients_.push_back(client);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
listener->set_parent(this);
|
||||
this->listeners_.push_back(listener);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
||||
this->raw_advertisements_ = false;
|
||||
this->parse_advertisements_ = false;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
|
||||
this->parse_advertisements_ = true;
|
||||
@@ -257,6 +268,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
||||
this->raw_advertisements_ = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
|
||||
this->parse_advertisements_ = true;
|
||||
@@ -264,6 +277,7 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
||||
this->raw_advertisements_ = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
@@ -282,10 +296,12 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
|
||||
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->gap_event_handler(event, param);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
||||
@@ -348,9 +364,11 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
|
||||
|
||||
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->gattc_event_handler(event, gattc_if, param);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
||||
@@ -704,12 +722,16 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
|
||||
// Process raw advertisements
|
||||
if (this->raw_advertisements_) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(&scan_result, 1);
|
||||
}
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(&scan_result, 1);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
// Process parsed advertisements
|
||||
@@ -719,16 +741,20 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
|
||||
device.parse_scan_rst(scan_result);
|
||||
|
||||
bool found = false;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
@@ -745,8 +771,10 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
// Reset timeout state machine instead of cancelling scheduler timeout
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
#endif
|
||||
|
||||
this->set_scanner_state_(ScannerState::IDLE);
|
||||
}
|
||||
@@ -770,6 +798,7 @@ void ESP32BLETracker::handle_scanner_failure_() {
|
||||
|
||||
void ESP32BLETracker::try_promote_discovered_clients_() {
|
||||
// Only promote the first discovered client to avoid multiple simultaneous connections
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->state() != ClientState::DISCOVERED) {
|
||||
continue;
|
||||
@@ -791,6 +820,7 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
|
||||
client->connect();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
|
||||
@@ -817,6 +847,7 @@ void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState
|
||||
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
if (force_ble && !this->coex_prefer_ble_) {
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
this->coex_prefer_ble_ = true;
|
||||
@@ -826,6 +857,7 @@ void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
||||
this->coex_prefer_ble_ = false;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
||||
}
|
||||
#endif // CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -302,6 +302,7 @@ class ESP32BLETracker : public Component,
|
||||
/// Count clients in each state
|
||||
ClientStateCounts count_client_states_() const {
|
||||
ClientStateCounts counts;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
for (auto *client : this->clients_) {
|
||||
switch (client->state()) {
|
||||
case ClientState::DISCONNECTING:
|
||||
@@ -317,12 +318,17 @@ class ESP32BLETracker : public Component,
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return counts;
|
||||
}
|
||||
|
||||
// Group 1: Large objects (12+ bytes) - vectors and callback manager
|
||||
std::vector<ESPBTDeviceListener *> listeners_;
|
||||
std::vector<ESPBTClient *> clients_;
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
|
||||
StaticVector<ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT> listeners_;
|
||||
#endif
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
|
||||
#endif
|
||||
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
/// Vector of addresses that have already been printed in print_bt_device_info
|
||||
|
||||
@@ -92,9 +92,14 @@ async def to_code(config):
|
||||
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.10.2")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
|
||||
if framework_ver >= cv.Version(5, 5, 0):
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
|
||||
else:
|
||||
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
|
||||
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
|
||||
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.0.11")
|
||||
esp32.add_extra_script(
|
||||
"post",
|
||||
"esp32_hosted.py",
|
||||
|
||||
@@ -42,6 +42,11 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
||||
symbols[i] = params->bit0;
|
||||
}
|
||||
}
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
|
||||
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
|
||||
*done = true;
|
||||
}
|
||||
#endif
|
||||
return RMT_SYMBOLS_PER_BYTE;
|
||||
}
|
||||
|
||||
|
||||
@@ -689,12 +689,9 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
|
||||
std::string EthernetComponent::get_use_address() const {
|
||||
if (this->use_address_.empty()) {
|
||||
return App.get_name() + ".local";
|
||||
}
|
||||
return this->use_address_;
|
||||
}
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const std::string &EthernetComponent::get_use_address() const { return this->use_address_; }
|
||||
|
||||
void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ class EthernetComponent : public Component {
|
||||
|
||||
network::IPAddresses get_ip_addresses();
|
||||
network::IPAddress get_dns_address(uint8_t num);
|
||||
std::string get_use_address() const;
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
void get_eth_mac_address_raw(uint8_t *mac);
|
||||
std::string get_eth_mac_address_pretty();
|
||||
|
||||
@@ -90,13 +90,12 @@ void HomeassistantNumber::control(float value) {
|
||||
api::HomeassistantActionRequest resp;
|
||||
resp.set_service(SERVICE_NAME);
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_id = resp.data.back();
|
||||
resp.data.init(2);
|
||||
auto &entity_id = resp.data.emplace_back();
|
||||
entity_id.set_key(ENTITY_ID_KEY);
|
||||
entity_id.value = this->entity_id_;
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_value = resp.data.back();
|
||||
auto &entity_value = resp.data.emplace_back();
|
||||
entity_value.set_key(VALUE_KEY);
|
||||
entity_value.value = to_string(value);
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ void HomeassistantSwitch::write_state(bool state) {
|
||||
resp.set_service(SERVICE_OFF);
|
||||
}
|
||||
|
||||
resp.data.emplace_back();
|
||||
auto &entity_id_kv = resp.data.back();
|
||||
resp.data.init(1);
|
||||
auto &entity_id_kv = resp.data.emplace_back();
|
||||
entity_id_kv.set_key(ENTITY_ID_KEY);
|
||||
entity_id_kv.value = this->entity_id_;
|
||||
|
||||
|
||||
@@ -167,8 +167,8 @@ class HttpRequestComponent : public Component {
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
|
||||
const std::string &body, const std::list<Header> &request_headers,
|
||||
std::set<std::string> collect_headers) = 0;
|
||||
const char *useragent_{nullptr};
|
||||
bool follow_redirects_{};
|
||||
|
||||
@@ -14,8 +14,9 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.arduino";
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
|
||||
const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
|
||||
@@ -31,8 +31,8 @@ class HttpContainerArduino : public HttpContainer {
|
||||
|
||||
class HttpRequestArduino : public HttpRequestComponent {
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> collect_headers) override;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,8 +17,9 @@ namespace http_request {
|
||||
|
||||
static const char *const TAG = "http_request.host";
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestHost::perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
|
||||
const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> response_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
|
||||
@@ -18,8 +18,8 @@ class HttpContainerHost : public HttpContainer {
|
||||
|
||||
class HttpRequestHost : public HttpRequestComponent {
|
||||
public:
|
||||
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> response_headers) override;
|
||||
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
|
||||
|
||||
|
||||
@@ -52,8 +52,9 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
|
||||
const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> collect_headers) {
|
||||
if (!network::is_connected()) {
|
||||
this->status_momentary_error("failed", 1000);
|
||||
|
||||
@@ -37,8 +37,8 @@ class HttpRequestIDF : public HttpRequestComponent {
|
||||
void set_buffer_size_tx(uint16_t buffer_size_tx) { this->buffer_size_tx_ = buffer_size_tx; }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<HttpContainer> perform(std::string url, std::string method, std::string body,
|
||||
std::list<Header> request_headers,
|
||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||
const std::list<Header> &request_headers,
|
||||
std::set<std::string> collect_headers) override;
|
||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||
uint16_t buffer_size_rx_{};
|
||||
|
||||
@@ -218,7 +218,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
}
|
||||
case improv::GET_WIFI_NETWORKS: {
|
||||
std::vector<std::string> networks;
|
||||
auto results = wifi::global_wifi_component->get_scan_result();
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (auto &scan : results) {
|
||||
if (scan.get_is_hidden())
|
||||
continue;
|
||||
|
||||
@@ -177,9 +177,10 @@ void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ =
|
||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const std::vector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::vector<LightEffect *> &effects) {
|
||||
this->effects_.reserve(this->effects_.size() + effects.size());
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_.init(effects.size());
|
||||
for (auto *effect : effects) {
|
||||
this->effects_.push_back(effect);
|
||||
}
|
||||
|
||||
@@ -11,8 +11,9 @@
|
||||
#include "light_traits.h"
|
||||
#include "light_transformer.h"
|
||||
|
||||
#include <vector>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <strings.h>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace light {
|
||||
@@ -159,7 +160,7 @@ class LightState : public EntityBase, public Component {
|
||||
bool supports_effects();
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const std::vector<LightEffect *> &get_effects() const;
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::vector<LightEffect *> &effects);
|
||||
@@ -260,7 +261,7 @@ class LightState : public EntityBase, public Component {
|
||||
/// The currently active transformer for this light (transition/flash).
|
||||
std::unique_ptr<LightTransformer> transformer_{nullptr};
|
||||
/// List of effects for this light.
|
||||
std::vector<LightEffect *> effects_;
|
||||
FixedVector<LightEffect *> effects_;
|
||||
/// Object used to store the persisted values of the light.
|
||||
ESPPreferenceObject rtc_;
|
||||
/// Value for storing the index of the currently active effect. 0 if no effect is active
|
||||
|
||||
@@ -486,7 +486,6 @@ CONF_RESUME_ON_INPUT = "resume_on_input"
|
||||
CONF_RIGHT_BUTTON = "right_button"
|
||||
CONF_ROLLOVER = "rollover"
|
||||
CONF_ROOT_BACK_BTN = "root_back_btn"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_SCALE_LINES = "scale_lines"
|
||||
CONF_SCROLLBAR_MODE = "scrollbar_mode"
|
||||
CONF_SELECTED_INDEX = "selected_index"
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.key_provider import KeyProvider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROWS, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from ..automation import action_to_code
|
||||
@@ -15,7 +15,6 @@ from ..defines import (
|
||||
CONF_ONE_CHECKED,
|
||||
CONF_PAD_COLUMN,
|
||||
CONF_PAD_ROW,
|
||||
CONF_ROWS,
|
||||
CONF_SELECTED,
|
||||
)
|
||||
from ..helpers import lvgl_components_required
|
||||
|
||||
@@ -2,7 +2,7 @@ from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import key_provider
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_ROWS, CONF_TRIGGER_ID
|
||||
|
||||
CODEOWNERS = ["@ssieb"]
|
||||
|
||||
@@ -19,7 +19,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
|
||||
)
|
||||
|
||||
CONF_KEYPAD_ID = "keypad_id"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_COLUMNS = "columns"
|
||||
CONF_KEYS = "keys"
|
||||
CONF_DEBOUNCE_TIME = "debounce_time"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DISABLED,
|
||||
@@ -125,6 +125,17 @@ def mdns_service(
|
||||
)
|
||||
|
||||
|
||||
def enable_mdns_storage():
|
||||
"""Enable persistent storage of mDNS services in the MDNSComponent.
|
||||
|
||||
Called by external components (like OpenThread) that need access to
|
||||
services after setup() completes via get_services().
|
||||
|
||||
Public API for external components. Do not remove.
|
||||
"""
|
||||
cg.add_define("USE_MDNS_STORE_SERVICES")
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.NETWORK_SERVICES)
|
||||
async def to_code(config):
|
||||
if config[CONF_DISABLED] is True:
|
||||
@@ -150,6 +161,8 @@ async def to_code(config):
|
||||
|
||||
if config[CONF_SERVICES]:
|
||||
cg.add_define("USE_MDNS_EXTRA_SERVICES")
|
||||
# Extra services need to be stored persistently
|
||||
enable_mdns_storage()
|
||||
|
||||
# Ensure at least 1 service (fallback service)
|
||||
cg.add_define("MDNS_SERVICE_COUNT", max(1, service_count))
|
||||
@@ -171,6 +184,10 @@ async def to_code(config):
|
||||
# Ensure at least 1 to avoid zero-size array
|
||||
cg.add_define("MDNS_DYNAMIC_TXT_COUNT", max(1, dynamic_txt_count))
|
||||
|
||||
# Enable storage if verbose logging is enabled (for dump_config)
|
||||
if get_logger_level() in ("VERBOSE", "VERY_VERBOSE"):
|
||||
enable_mdns_storage()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
|
||||
// Wrap build-time defines into flash storage
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
|
||||
|
||||
void MDNSComponent::compile_records_() {
|
||||
void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services) {
|
||||
this->hostname_ = App.get_name();
|
||||
|
||||
// IMPORTANT: The #ifdef blocks below must match COMPONENTS_WITH_MDNS_SERVICES
|
||||
@@ -53,7 +53,7 @@ void MDNSComponent::compile_records_() {
|
||||
MDNS_STATIC_CONST_CHAR(VALUE_BOARD, ESPHOME_BOARD);
|
||||
|
||||
if (api::global_api_server != nullptr) {
|
||||
auto &service = this->services_.emplace_next();
|
||||
auto &service = services.emplace_next();
|
||||
service.service_type = MDNS_STR(SERVICE_ESPHOMELIB);
|
||||
service.proto = MDNS_STR(SERVICE_TCP);
|
||||
service.port = api::global_api_server->get_port();
|
||||
@@ -83,7 +83,7 @@ void MDNSComponent::compile_records_() {
|
||||
#endif
|
||||
|
||||
auto &txt_records = service.txt_records;
|
||||
txt_records.reserve(txt_count);
|
||||
txt_records.init(txt_count);
|
||||
|
||||
if (!friendly_name_empty) {
|
||||
txt_records.push_back({MDNS_STR(TXT_FRIENDLY_NAME), MDNS_STR(friendly_name.c_str())});
|
||||
@@ -146,7 +146,7 @@ void MDNSComponent::compile_records_() {
|
||||
#ifdef USE_PROMETHEUS
|
||||
MDNS_STATIC_CONST_CHAR(SERVICE_PROMETHEUS, "_prometheus-http");
|
||||
|
||||
auto &prom_service = this->services_.emplace_next();
|
||||
auto &prom_service = services.emplace_next();
|
||||
prom_service.service_type = MDNS_STR(SERVICE_PROMETHEUS);
|
||||
prom_service.proto = MDNS_STR(SERVICE_TCP);
|
||||
prom_service.port = USE_WEBSERVER_PORT;
|
||||
@@ -155,7 +155,7 @@ void MDNSComponent::compile_records_() {
|
||||
#ifdef USE_WEBSERVER
|
||||
MDNS_STATIC_CONST_CHAR(SERVICE_HTTP, "_http");
|
||||
|
||||
auto &web_service = this->services_.emplace_next();
|
||||
auto &web_service = services.emplace_next();
|
||||
web_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||
web_service.proto = MDNS_STR(SERVICE_TCP);
|
||||
web_service.port = USE_WEBSERVER_PORT;
|
||||
@@ -167,11 +167,11 @@ void MDNSComponent::compile_records_() {
|
||||
|
||||
// Publish "http" service if not using native API or any other services
|
||||
// This is just to have *some* mDNS service so that .local resolution works
|
||||
auto &fallback_service = this->services_.emplace_next();
|
||||
auto &fallback_service = services.emplace_next();
|
||||
fallback_service.service_type = MDNS_STR(SERVICE_HTTP);
|
||||
fallback_service.proto = MDNS_STR(SERVICE_TCP);
|
||||
fallback_service.port = USE_WEBSERVER_PORT;
|
||||
fallback_service.txt_records.push_back({MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)});
|
||||
fallback_service.txt_records = {{MDNS_STR(TXT_VERSION), MDNS_STR(VALUE_VERSION)}};
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -180,7 +180,7 @@ void MDNSComponent::dump_config() {
|
||||
"mDNS:\n"
|
||||
" Hostname: %s",
|
||||
this->hostname_.c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
ESP_LOGV(TAG, " Services:");
|
||||
for (const auto &service : this->services_) {
|
||||
ESP_LOGV(TAG, " - %s, %s, %d", MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto),
|
||||
|
||||
@@ -38,7 +38,7 @@ struct MDNSService {
|
||||
// as defined in RFC6763 Section 7, like "_tcp" or "_udp"
|
||||
const MDNSString *proto;
|
||||
TemplatableValue<uint16_t> port;
|
||||
std::vector<MDNSTXTRecord> txt_records;
|
||||
FixedVector<MDNSTXTRecord> txt_records;
|
||||
};
|
||||
|
||||
class MDNSComponent : public Component {
|
||||
@@ -55,7 +55,9 @@ class MDNSComponent : public Component {
|
||||
void add_extra_service(MDNSService service) { this->services_.emplace_next() = std::move(service); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
const StaticVector<MDNSService, MDNS_SERVICE_COUNT> &get_services() const { return this->services_; }
|
||||
#endif
|
||||
|
||||
void on_shutdown() override;
|
||||
|
||||
@@ -71,9 +73,11 @@ class MDNSComponent : public Component {
|
||||
StaticVector<std::string, MDNS_DYNAMIC_TXT_COUNT> dynamic_txt_values_;
|
||||
|
||||
protected:
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
|
||||
#endif
|
||||
std::string hostname_;
|
||||
void compile_records_();
|
||||
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services);
|
||||
};
|
||||
|
||||
} // namespace mdns
|
||||
|
||||
@@ -12,7 +12,13 @@ namespace mdns {
|
||||
static const char *const TAG = "mdns";
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->compile_records_();
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
esp_err_t err = mdns_init();
|
||||
if (err != ESP_OK) {
|
||||
@@ -24,7 +30,7 @@ void MDNSComponent::setup() {
|
||||
mdns_hostname_set(this->hostname_.c_str());
|
||||
mdns_instance_name_set(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : this->services_) {
|
||||
for (const auto &service : services) {
|
||||
std::vector<mdns_txt_item_t> txt_records;
|
||||
for (const auto &record : service.txt_records) {
|
||||
mdns_txt_item_t it{};
|
||||
|
||||
@@ -12,11 +12,17 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->compile_records_();
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : this->services_) {
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
// part of the wire protocol to have an underscore, and for example ESP-IDF
|
||||
// expects the underscore to be there, the ESP8266 implementation always adds
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() { this->compile_records_(); }
|
||||
void MDNSComponent::setup() {
|
||||
// Host platform doesn't have actual mDNS implementation
|
||||
}
|
||||
|
||||
void MDNSComponent::on_shutdown() {}
|
||||
|
||||
|
||||
@@ -12,11 +12,17 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->compile_records_();
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : this->services_) {
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
// part of the wire protocol to have an underscore, and for example ESP-IDF
|
||||
// expects the underscore to be there, the ESP8266 implementation always adds
|
||||
|
||||
@@ -12,11 +12,17 @@ namespace esphome {
|
||||
namespace mdns {
|
||||
|
||||
void MDNSComponent::setup() {
|
||||
this->compile_records_();
|
||||
#ifdef USE_MDNS_STORE_SERVICES
|
||||
this->compile_records_(this->services_);
|
||||
const auto &services = this->services_;
|
||||
#else
|
||||
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services;
|
||||
this->compile_records_(services);
|
||||
#endif
|
||||
|
||||
MDNS.begin(this->hostname_.c_str());
|
||||
|
||||
for (const auto &service : this->services_) {
|
||||
for (const auto &service : services) {
|
||||
// Strip the leading underscore from the proto and service_type. While it is
|
||||
// part of the wire protocol to have an underscore, and for example ESP-IDF
|
||||
// expects the underscore to be there, the ESP8266 implementation always adds
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_BRIGHTNESS,
|
||||
CONF_COLOR_ORDER,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_HEIGHT,
|
||||
CONF_INIT_SEQUENCE,
|
||||
CONF_INVERT_COLORS,
|
||||
@@ -301,6 +302,8 @@ class DriverChip:
|
||||
Check if a rotation can be implemented in hardware using the MADCTL register.
|
||||
A rotation of 180 is always possible if x and y mirroring are supported, 90 and 270 are possible if the model supports swapping X and Y.
|
||||
"""
|
||||
if config.get(CONF_TRANSFORM) == CONF_DISABLED:
|
||||
return False
|
||||
transforms = self.transforms
|
||||
rotation = config.get(CONF_ROTATION, 0)
|
||||
if rotation == 0 or not transforms:
|
||||
@@ -358,26 +361,26 @@ class DriverChip:
|
||||
CONF_SWAP_XY: self.get_default(CONF_SWAP_XY),
|
||||
},
|
||||
)
|
||||
# fill in defaults if not provided
|
||||
mirror_x = transform.get(CONF_MIRROR_X, self.get_default(CONF_MIRROR_X))
|
||||
mirror_y = transform.get(CONF_MIRROR_Y, self.get_default(CONF_MIRROR_Y))
|
||||
swap_xy = transform.get(CONF_SWAP_XY, self.get_default(CONF_SWAP_XY))
|
||||
transform[CONF_MIRROR_X] = mirror_x
|
||||
transform[CONF_MIRROR_Y] = mirror_y
|
||||
transform[CONF_SWAP_XY] = swap_xy
|
||||
|
||||
if not isinstance(transform, dict):
|
||||
# Presumably disabled
|
||||
return {
|
||||
CONF_MIRROR_X: False,
|
||||
CONF_MIRROR_Y: False,
|
||||
CONF_SWAP_XY: False,
|
||||
CONF_TRANSFORM: False,
|
||||
}
|
||||
# Can we use the MADCTL register to set the rotation?
|
||||
if can_transform and CONF_TRANSFORM not in config:
|
||||
rotation = config[CONF_ROTATION]
|
||||
if rotation == 180:
|
||||
transform[CONF_MIRROR_X] = not mirror_x
|
||||
transform[CONF_MIRROR_Y] = not mirror_y
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
elif rotation == 90:
|
||||
transform[CONF_SWAP_XY] = not swap_xy
|
||||
transform[CONF_MIRROR_X] = not mirror_x
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
|
||||
else:
|
||||
transform[CONF_SWAP_XY] = not swap_xy
|
||||
transform[CONF_MIRROR_Y] = not mirror_y
|
||||
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
|
||||
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
|
||||
transform[CONF_TRANSFORM] = True
|
||||
return transform
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ from esphome.const import (
|
||||
CONF_DATA_RATE,
|
||||
CONF_DC_PIN,
|
||||
CONF_DIMENSIONS,
|
||||
CONF_DISABLED,
|
||||
CONF_ENABLE_PIN,
|
||||
CONF_ID,
|
||||
CONF_INIT_SEQUENCE,
|
||||
@@ -146,12 +147,15 @@ def swap_xy_schema(model):
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
bus_mode = config[CONF_BUS_MODE]
|
||||
transform = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
**swap_xy_schema(model),
|
||||
}
|
||||
transform = cv.Any(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
cv.Required(CONF_MIRROR_Y): cv.boolean,
|
||||
**swap_xy_schema(model),
|
||||
}
|
||||
),
|
||||
cv.one_of(CONF_DISABLED, lower=True),
|
||||
)
|
||||
# CUSTOM model will need to provide a custom init sequence
|
||||
iseqconf = (
|
||||
@@ -160,7 +164,11 @@ def model_schema(config):
|
||||
else cv.Optional(CONF_INIT_SEQUENCE)
|
||||
)
|
||||
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
|
||||
is_swapped = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY) is True
|
||||
transform_config = config.get(CONF_TRANSFORM, {})
|
||||
is_swapped = (
|
||||
isinstance(transform_config, dict)
|
||||
and transform_config.get(CONF_SWAP_XY, False) is True
|
||||
)
|
||||
cv_dimensions = (
|
||||
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
|
||||
)
|
||||
@@ -192,9 +200,7 @@ def model_schema(config):
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(MipiSpi),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(
|
||||
model.get_default(CONF_DRAW_ROUNDING, 1)
|
||||
),
|
||||
cv_dimensions(CONF_DIMENSIONS): dimension_schema(1),
|
||||
model.option(CONF_ENABLE_PIN, cv.UNDEFINED): cv.ensure_list(
|
||||
pins.gpio_output_pin_schema
|
||||
),
|
||||
@@ -400,6 +406,7 @@ def get_instance(config):
|
||||
offset_height,
|
||||
DISPLAY_ROTATIONS[rotation],
|
||||
frac,
|
||||
config[CONF_DRAW_ROUNDING],
|
||||
]
|
||||
)
|
||||
return MipiSpiBuffer, templateargs
|
||||
@@ -431,7 +438,6 @@ async def to_code(config):
|
||||
else:
|
||||
config[CONF_ROTATION] = 0
|
||||
cg.add(var.set_model(config[CONF_MODEL]))
|
||||
cg.add(var.set_draw_rounding(config[CONF_DRAW_ROUNDING]))
|
||||
if enable_pin := config.get(CONF_ENABLE_PIN):
|
||||
enable = [await cg.gpio_pin_expression(pin) for pin in enable_pin]
|
||||
cg.add(var.set_enable_pins(enable))
|
||||
|
||||
@@ -38,7 +38,7 @@ static constexpr uint8_t MADCTL_BGR = 0x08; // Bit 3 Blue-Green-Red pixel ord
|
||||
static constexpr uint8_t MADCTL_XFLIP = 0x02; // Mirror the display horizontally
|
||||
static constexpr uint8_t MADCTL_YFLIP = 0x01; // Mirror the display vertically
|
||||
|
||||
static const uint8_t DELAY_FLAG = 0xFF;
|
||||
static constexpr uint8_t DELAY_FLAG = 0xFF;
|
||||
// store a 16 bit value in a buffer, big endian.
|
||||
static inline void put16_be(uint8_t *buf, uint16_t value) {
|
||||
buf[0] = value >> 8;
|
||||
@@ -79,7 +79,7 @@ class MipiSpi : public display::Display,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
MipiSpi() {}
|
||||
MipiSpi() = default;
|
||||
void update() override { this->stop_poller(); }
|
||||
void draw_pixel_at(int x, int y, Color color) override {}
|
||||
void set_model(const char *model) { this->model_ = model; }
|
||||
@@ -99,7 +99,6 @@ class MipiSpi : public display::Display,
|
||||
int get_width_internal() override { return WIDTH; }
|
||||
int get_height_internal() override { return HEIGHT; }
|
||||
void set_init_sequence(const std::vector<uint8_t> &sequence) { this->init_sequence_ = sequence; }
|
||||
void set_draw_rounding(unsigned rounding) { this->draw_rounding_ = rounding; }
|
||||
|
||||
// reset the display, and write the init sequence
|
||||
void setup() override {
|
||||
@@ -326,6 +325,7 @@ class MipiSpi : public display::Display,
|
||||
|
||||
/**
|
||||
* Writes a buffer to the display.
|
||||
* @param ptr The pointer to the pixel data
|
||||
* @param w Width of each line in bytes
|
||||
* @param h Height of the buffer in rows
|
||||
* @param pad Padding in bytes after each line
|
||||
@@ -424,7 +424,6 @@ class MipiSpi : public display::Display,
|
||||
|
||||
// other properties set by configuration
|
||||
bool invert_colors_{};
|
||||
unsigned draw_rounding_{2};
|
||||
optional<uint8_t> brightness_{};
|
||||
const char *model_{"Unknown"};
|
||||
std::vector<uint8_t> init_sequence_{};
|
||||
@@ -444,12 +443,20 @@ class MipiSpi : public display::Display,
|
||||
* @tparam OFFSET_WIDTH The x-offset of the display in pixels
|
||||
* @tparam OFFSET_HEIGHT The y-offset of the display in pixels
|
||||
* @tparam FRACTION The fraction of the display size to use for the buffer (e.g. 4 means a 1/4 buffer).
|
||||
* @tparam ROUNDING The alignment requirement for drawing operations (e.g. 2 means that x coordinates must be even)
|
||||
*/
|
||||
template<typename BUFFERTYPE, PixelMode BUFFERPIXEL, bool IS_BIG_ENDIAN, PixelMode DISPLAYPIXEL, BusType BUS_TYPE,
|
||||
int WIDTH, int HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION, int FRACTION>
|
||||
uint16_t WIDTH, uint16_t HEIGHT, int OFFSET_WIDTH, int OFFSET_HEIGHT, display::DisplayRotation ROTATION,
|
||||
int FRACTION, unsigned ROUNDING>
|
||||
class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT,
|
||||
OFFSET_WIDTH, OFFSET_HEIGHT> {
|
||||
public:
|
||||
// these values define the buffer size needed to write in accordance with the chip pixel alignment
|
||||
// requirements. If the required rounding does not divide the width and height, we round up to the next multiple and
|
||||
// ignore the extra columns and rows when drawing, but use them to write to the display.
|
||||
static constexpr unsigned BUFFER_WIDTH = (WIDTH + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||
static constexpr unsigned BUFFER_HEIGHT = (HEIGHT + ROUNDING - 1) / ROUNDING * ROUNDING;
|
||||
|
||||
MipiSpiBuffer() { this->rotation_ = ROTATION; }
|
||||
|
||||
void dump_config() override {
|
||||
@@ -461,15 +468,15 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
" Buffer fraction: 1/%d\n"
|
||||
" Buffer bytes: %zu\n"
|
||||
" Draw rounding: %u",
|
||||
this->rotation_, BUFFERPIXEL * 8, FRACTION, sizeof(BUFFERTYPE) * WIDTH * HEIGHT / FRACTION,
|
||||
this->draw_rounding_);
|
||||
this->rotation_, BUFFERPIXEL * 8, FRACTION,
|
||||
sizeof(BUFFERTYPE) * BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION, ROUNDING);
|
||||
}
|
||||
|
||||
void setup() override {
|
||||
MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DISPLAYPIXEL, BUS_TYPE, WIDTH, HEIGHT, OFFSET_WIDTH,
|
||||
OFFSET_HEIGHT>::setup();
|
||||
RAMAllocator<BUFFERTYPE> allocator{};
|
||||
this->buffer_ = allocator.allocate(WIDTH * HEIGHT / FRACTION);
|
||||
this->buffer_ = allocator.allocate(BUFFER_WIDTH * BUFFER_HEIGHT / FRACTION);
|
||||
if (this->buffer_ == nullptr) {
|
||||
this->mark_failed("Buffer allocation failed");
|
||||
}
|
||||
@@ -508,15 +515,14 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
esph_log_v(TAG, "x_low %d, y_low %d, x_high %d, y_high %d", this->x_low_, this->y_low_, this->x_high_,
|
||||
this->y_high_);
|
||||
// Some chips require that the drawing window be aligned on certain boundaries
|
||||
auto dr = this->draw_rounding_;
|
||||
this->x_low_ = this->x_low_ / dr * dr;
|
||||
this->y_low_ = this->y_low_ / dr * dr;
|
||||
this->x_high_ = (this->x_high_ + dr) / dr * dr - 1;
|
||||
this->y_high_ = (this->y_high_ + dr) / dr * dr - 1;
|
||||
this->x_low_ = this->x_low_ / ROUNDING * ROUNDING;
|
||||
this->y_low_ = this->y_low_ / ROUNDING * ROUNDING;
|
||||
this->x_high_ = (this->x_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
this->y_high_ = (this->y_high_ + ROUNDING) / ROUNDING * ROUNDING - 1;
|
||||
int w = this->x_high_ - this->x_low_ + 1;
|
||||
int h = this->y_high_ - this->y_low_ + 1;
|
||||
this->write_to_display_(this->x_low_, this->y_low_, w, h, this->buffer_, this->x_low_,
|
||||
this->y_low_ - this->start_line_, WIDTH - w);
|
||||
this->y_low_ - this->start_line_, BUFFER_WIDTH - w);
|
||||
// invalidate watermarks
|
||||
this->x_low_ = WIDTH;
|
||||
this->y_low_ = HEIGHT;
|
||||
@@ -536,10 +542,10 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
void draw_pixel_at(int x, int y, Color color) override {
|
||||
if (!this->get_clipping().inside(x, y))
|
||||
return;
|
||||
rotate_coordinates_(x, y);
|
||||
rotate_coordinates(x, y);
|
||||
if (x < 0 || x >= WIDTH || y < this->start_line_ || y >= this->end_line_)
|
||||
return;
|
||||
this->buffer_[(y - this->start_line_) * WIDTH + x] = convert_color_(color);
|
||||
this->buffer_[(y - this->start_line_) * BUFFER_WIDTH + x] = convert_color(color);
|
||||
if (x < this->x_low_) {
|
||||
this->x_low_ = x;
|
||||
}
|
||||
@@ -560,7 +566,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
this->y_low_ = this->start_line_;
|
||||
this->x_high_ = WIDTH - 1;
|
||||
this->y_high_ = this->end_line_ - 1;
|
||||
std::fill_n(this->buffer_, HEIGHT * WIDTH / FRACTION, convert_color_(color));
|
||||
std::fill_n(this->buffer_, HEIGHT * BUFFER_WIDTH / FRACTION, convert_color(color));
|
||||
}
|
||||
|
||||
int get_width() override {
|
||||
@@ -577,7 +583,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
|
||||
protected:
|
||||
// Rotate the coordinates to match the display orientation.
|
||||
void rotate_coordinates_(int &x, int &y) const {
|
||||
static void rotate_coordinates(int &x, int &y) {
|
||||
if constexpr (ROTATION == display::DISPLAY_ROTATION_180_DEGREES) {
|
||||
x = WIDTH - x - 1;
|
||||
y = HEIGHT - y - 1;
|
||||
@@ -593,7 +599,7 @@ class MipiSpiBuffer : public MipiSpi<BUFFERTYPE, BUFFERPIXEL, IS_BIG_ENDIAN, DIS
|
||||
}
|
||||
|
||||
// Convert a color to the buffer pixel format.
|
||||
BUFFERTYPE convert_color_(Color &color) const {
|
||||
static BUFFERTYPE convert_color(const Color &color) {
|
||||
if constexpr (BUFFERPIXEL == PIXEL_MODE_8) {
|
||||
return (color.red & 0xE0) | (color.g & 0xE0) >> 3 | color.b >> 6;
|
||||
} else if constexpr (BUFFERPIXEL == PIXEL_MODE_16) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.config_validation as cv
|
||||
|
||||
from .amoled import CO5300
|
||||
from .ili import ILI9488_A
|
||||
from .jc import AXS15231
|
||||
|
||||
DriverChip(
|
||||
"WAVESHARE-4-TFT",
|
||||
@@ -152,3 +153,12 @@ CO5300.extend(
|
||||
cs_pin=12,
|
||||
reset_pin=39,
|
||||
)
|
||||
|
||||
AXS15231.extend(
|
||||
"WAVESHARE-ESP32-S3-TOUCH-LCD-3.49",
|
||||
width=172,
|
||||
height=640,
|
||||
data_rate="80MHz",
|
||||
cs_pin=9,
|
||||
reset_pin=21,
|
||||
)
|
||||
|
||||
@@ -29,7 +29,8 @@ static const char *const TAG = "mqtt";
|
||||
|
||||
MQTTClientComponent::MQTTClientComponent() {
|
||||
global_mqtt_client = this;
|
||||
this->credentials_.client_id = App.get_name() + "-" + get_mac_address();
|
||||
const std::string mac_addr = get_mac_address();
|
||||
this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr.c_str(), mac_addr.size());
|
||||
}
|
||||
|
||||
// Connection
|
||||
|
||||
@@ -85,22 +85,25 @@ network::IPAddresses get_ip_addresses() {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string get_use_address() {
|
||||
const std::string &get_use_address() {
|
||||
// Global component pointers are guaranteed to be set by component constructors when USE_* is defined
|
||||
#ifdef USE_ETHERNET
|
||||
if (ethernet::global_eth_component != nullptr)
|
||||
return ethernet::global_eth_component->get_use_address();
|
||||
return ethernet::global_eth_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#ifdef USE_MODEM
|
||||
if (modem::global_modem_component != nullptr)
|
||||
return modem::global_modem_component->get_use_address();
|
||||
return modem::global_modem_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component != nullptr)
|
||||
return wifi::global_wifi_component->get_use_address();
|
||||
return wifi::global_wifi_component->get_use_address();
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI)
|
||||
// Fallback when no network component is defined (e.g., host platform)
|
||||
static const std::string empty;
|
||||
return empty;
|
||||
#endif
|
||||
return "";
|
||||
}
|
||||
|
||||
} // namespace network
|
||||
|
||||
@@ -12,7 +12,7 @@ bool is_connected();
|
||||
/// Return whether the network is disabled (only wifi for now)
|
||||
bool is_disabled();
|
||||
/// Get the active network hostname
|
||||
std::string get_use_address();
|
||||
const std::string &get_use_address();
|
||||
IPAddresses get_ip_addresses();
|
||||
|
||||
} // namespace network
|
||||
|
||||
@@ -5,7 +5,7 @@ from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
only_on_variant,
|
||||
)
|
||||
from esphome.components.mdns import MDNSComponent
|
||||
from esphome.components.mdns import MDNSComponent, enable_mdns_storage
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID
|
||||
import esphome.final_validate as fv
|
||||
@@ -141,6 +141,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_OPENTHREAD")
|
||||
|
||||
# OpenThread SRP needs access to mDNS services after setup
|
||||
enable_mdns_storage()
|
||||
|
||||
ot = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(ot, config)
|
||||
|
||||
|
||||
@@ -145,7 +145,7 @@ class BSDSocketImpl : public Socket {
|
||||
}
|
||||
|
||||
ssize_t sendto(const void *buf, size_t len, int flags, const struct sockaddr *to, socklen_t tolen) override {
|
||||
return ::sendto(fd_, buf, len, flags, to, tolen);
|
||||
return ::sendto(fd_, buf, len, flags, to, tolen); // NOLINT(readability-suspicious-call-argument)
|
||||
}
|
||||
|
||||
int setblocking(bool blocking) override {
|
||||
|
||||
@@ -40,33 +40,14 @@ class LWIPRawImpl : public Socket {
|
||||
void init() {
|
||||
LWIP_LOG("init(%p)", pcb_);
|
||||
tcp_arg(pcb_, this);
|
||||
tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
|
||||
tcp_recv(pcb_, LWIPRawImpl::s_recv_fn);
|
||||
tcp_err(pcb_, LWIPRawImpl::s_err_fn);
|
||||
}
|
||||
|
||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return nullptr;
|
||||
}
|
||||
if (this->accepted_socket_count_ == 0) {
|
||||
errno = EWOULDBLOCK;
|
||||
return nullptr;
|
||||
}
|
||||
// Take from front for FIFO ordering
|
||||
std::unique_ptr<LWIPRawImpl> sock = std::move(this->accepted_sockets_[0]);
|
||||
// Shift remaining sockets forward
|
||||
for (uint8_t i = 1; i < this->accepted_socket_count_; i++) {
|
||||
this->accepted_sockets_[i - 1] = std::move(this->accepted_sockets_[i]);
|
||||
}
|
||||
this->accepted_socket_count_--;
|
||||
LWIP_LOG("Connection accepted by application, queue size: %d", this->accepted_socket_count_);
|
||||
if (addr != nullptr) {
|
||||
sock->getpeername(addr, addrlen);
|
||||
}
|
||||
LWIP_LOG("accept(%p)", sock.get());
|
||||
return std::unique_ptr<Socket>(std::move(sock));
|
||||
// Non-listening sockets return error
|
||||
errno = EINVAL;
|
||||
return nullptr;
|
||||
}
|
||||
int bind(const struct sockaddr *name, socklen_t addrlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
@@ -292,25 +273,10 @@ class LWIPRawImpl : public Socket {
|
||||
return -1;
|
||||
}
|
||||
int listen(int backlog) override {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog);
|
||||
struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog);
|
||||
if (listen_pcb == nullptr) {
|
||||
tcp_abort(pcb_);
|
||||
pcb_ = nullptr;
|
||||
errno = EOPNOTSUPP;
|
||||
return -1;
|
||||
}
|
||||
// tcp_listen reallocates the pcb, replace ours
|
||||
pcb_ = listen_pcb;
|
||||
// set callbacks on new pcb
|
||||
LWIP_LOG("tcp_arg(%p)", pcb_);
|
||||
tcp_arg(pcb_, this);
|
||||
tcp_accept(pcb_, LWIPRawImpl::s_accept_fn);
|
||||
return 0;
|
||||
// Regular sockets can't be converted to listening - this shouldn't happen
|
||||
// as listen() should only be called on sockets created for listening
|
||||
errno = EOPNOTSUPP;
|
||||
return -1;
|
||||
}
|
||||
ssize_t read(void *buf, size_t len) override {
|
||||
if (pcb_ == nullptr) {
|
||||
@@ -491,29 +457,6 @@ class LWIPRawImpl : public Socket {
|
||||
return 0;
|
||||
}
|
||||
|
||||
err_t accept_fn(struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
|
||||
if (err != ERR_OK || newpcb == nullptr) {
|
||||
// "An error code if there has been an error accepting. Only return ERR_ABRT if you have
|
||||
// called tcp_abort from within the callback function!"
|
||||
// https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
|
||||
// nothing to do here, we just don't push it to the queue
|
||||
return ERR_OK;
|
||||
}
|
||||
// Check if we've reached the maximum accept queue size
|
||||
if (this->accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
|
||||
LWIP_LOG("Rejecting connection, queue full (%d)", this->accepted_socket_count_);
|
||||
// Abort the connection when queue is full
|
||||
tcp_abort(newpcb);
|
||||
// Must return ERR_ABRT since we called tcp_abort()
|
||||
return ERR_ABRT;
|
||||
}
|
||||
auto sock = make_unique<LWIPRawImpl>(family_, newpcb);
|
||||
sock->init();
|
||||
this->accepted_sockets_[this->accepted_socket_count_++] = std::move(sock);
|
||||
LWIP_LOG("Accepted connection, queue size: %d", this->accepted_socket_count_);
|
||||
return ERR_OK;
|
||||
}
|
||||
void err_fn(err_t err) {
|
||||
LWIP_LOG("err(err=%d)", err);
|
||||
// "If a connection is aborted because of an error, the application is alerted of this event by
|
||||
@@ -545,11 +488,6 @@ class LWIPRawImpl : public Socket {
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
return arg_this->accept_fn(newpcb, err);
|
||||
}
|
||||
|
||||
static void s_err_fn(void *arg, err_t err) {
|
||||
LWIPRawImpl *arg_this = reinterpret_cast<LWIPRawImpl *>(arg);
|
||||
arg_this->err_fn(err);
|
||||
@@ -601,7 +539,107 @@ class LWIPRawImpl : public Socket {
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Member ordering optimized to minimize padding on 32-bit systems
|
||||
// Largest members first (4 bytes), then smaller members (1 byte each)
|
||||
struct tcp_pcb *pcb_;
|
||||
pbuf *rx_buf_ = nullptr;
|
||||
size_t rx_buf_offset_ = 0;
|
||||
bool rx_closed_ = false;
|
||||
// don't use lwip nodelay flag, it sometimes causes reconnect
|
||||
// instead use it for determining whether to call lwip_output
|
||||
bool nodelay_ = false;
|
||||
sa_family_t family_ = 0;
|
||||
};
|
||||
|
||||
// Listening socket class - only allocates accept queue when needed (for bind+listen sockets)
|
||||
// This saves 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) for regular connected sockets on ESP8266/RP2040
|
||||
class LWIPRawListenImpl : public LWIPRawImpl {
|
||||
public:
|
||||
LWIPRawListenImpl(sa_family_t family, struct tcp_pcb *pcb) : LWIPRawImpl(family, pcb) {}
|
||||
|
||||
void init() {
|
||||
LWIP_LOG("init(%p)", pcb_);
|
||||
tcp_arg(pcb_, this);
|
||||
tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn);
|
||||
tcp_err(pcb_, LWIPRawImpl::s_err_fn); // Use base class error handler
|
||||
}
|
||||
|
||||
std::unique_ptr<Socket> accept(struct sockaddr *addr, socklen_t *addrlen) override {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return nullptr;
|
||||
}
|
||||
if (accepted_socket_count_ == 0) {
|
||||
errno = EWOULDBLOCK;
|
||||
return nullptr;
|
||||
}
|
||||
// Take from front for FIFO ordering
|
||||
std::unique_ptr<LWIPRawImpl> sock = std::move(accepted_sockets_[0]);
|
||||
// Shift remaining sockets forward
|
||||
for (uint8_t i = 1; i < accepted_socket_count_; i++) {
|
||||
accepted_sockets_[i - 1] = std::move(accepted_sockets_[i]);
|
||||
}
|
||||
accepted_socket_count_--;
|
||||
LWIP_LOG("Connection accepted by application, queue size: %d", accepted_socket_count_);
|
||||
if (addr != nullptr) {
|
||||
sock->getpeername(addr, addrlen);
|
||||
}
|
||||
LWIP_LOG("accept(%p)", sock.get());
|
||||
return std::unique_ptr<Socket>(std::move(sock));
|
||||
}
|
||||
|
||||
int listen(int backlog) override {
|
||||
if (pcb_ == nullptr) {
|
||||
errno = EBADF;
|
||||
return -1;
|
||||
}
|
||||
LWIP_LOG("tcp_listen_with_backlog(%p backlog=%d)", pcb_, backlog);
|
||||
struct tcp_pcb *listen_pcb = tcp_listen_with_backlog(pcb_, backlog);
|
||||
if (listen_pcb == nullptr) {
|
||||
tcp_abort(pcb_);
|
||||
pcb_ = nullptr;
|
||||
errno = EOPNOTSUPP;
|
||||
return -1;
|
||||
}
|
||||
// tcp_listen reallocates the pcb, replace ours
|
||||
pcb_ = listen_pcb;
|
||||
// set callbacks on new pcb
|
||||
LWIP_LOG("tcp_arg(%p)", pcb_);
|
||||
tcp_arg(pcb_, this);
|
||||
tcp_accept(pcb_, LWIPRawListenImpl::s_accept_fn);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private:
|
||||
err_t accept_fn(struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIP_LOG("accept(newpcb=%p err=%d)", newpcb, err);
|
||||
if (err != ERR_OK || newpcb == nullptr) {
|
||||
// "An error code if there has been an error accepting. Only return ERR_ABRT if you have
|
||||
// called tcp_abort from within the callback function!"
|
||||
// https://www.nongnu.org/lwip/2_1_x/tcp_8h.html#a00517abce6856d6c82f0efebdafb734d
|
||||
// nothing to do here, we just don't push it to the queue
|
||||
return ERR_OK;
|
||||
}
|
||||
// Check if we've reached the maximum accept queue size
|
||||
if (accepted_socket_count_ >= MAX_ACCEPTED_SOCKETS) {
|
||||
LWIP_LOG("Rejecting connection, queue full (%d)", accepted_socket_count_);
|
||||
// Abort the connection when queue is full
|
||||
tcp_abort(newpcb);
|
||||
// Must return ERR_ABRT since we called tcp_abort()
|
||||
return ERR_ABRT;
|
||||
}
|
||||
auto sock = make_unique<LWIPRawImpl>(family_, newpcb);
|
||||
sock->init();
|
||||
accepted_sockets_[accepted_socket_count_++] = std::move(sock);
|
||||
LWIP_LOG("Accepted connection, queue size: %d", accepted_socket_count_);
|
||||
return ERR_OK;
|
||||
}
|
||||
|
||||
static err_t s_accept_fn(void *arg, struct tcp_pcb *newpcb, err_t err) {
|
||||
LWIPRawListenImpl *arg_this = reinterpret_cast<LWIPRawListenImpl *>(arg);
|
||||
return arg_this->accept_fn(newpcb, err);
|
||||
}
|
||||
|
||||
// Accept queue - holds incoming connections briefly until the event loop calls accept()
|
||||
// This is NOT a connection pool - just a temporary queue between LWIP callbacks and the main loop
|
||||
// 3 slots is plenty since connections are pulled out quickly by the event loop
|
||||
@@ -613,23 +651,21 @@ class LWIPRawImpl : public Socket {
|
||||
// - std::array<3>: 12 bytes fixed (3 pointers × 4 bytes)
|
||||
// Saves ~44+ bytes RAM per listening socket + avoids ALL heap allocations
|
||||
// Used on ESP8266 and RP2040 (platforms using LWIP_TCP implementation)
|
||||
//
|
||||
// By using a separate listening socket class, regular connected sockets save
|
||||
// 16 bytes (12 bytes array + 1 byte count + 3 bytes padding) of memory overhead on 32-bit systems
|
||||
static constexpr size_t MAX_ACCEPTED_SOCKETS = 3;
|
||||
std::array<std::unique_ptr<LWIPRawImpl>, MAX_ACCEPTED_SOCKETS> accepted_sockets_;
|
||||
uint8_t accepted_socket_count_ = 0; // Number of sockets currently in queue
|
||||
bool rx_closed_ = false;
|
||||
pbuf *rx_buf_ = nullptr;
|
||||
size_t rx_buf_offset_ = 0;
|
||||
// don't use lwip nodelay flag, it sometimes causes reconnect
|
||||
// instead use it for determining whether to call lwip_output
|
||||
bool nodelay_ = false;
|
||||
sa_family_t family_ = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<Socket> socket(int domain, int type, int protocol) {
|
||||
auto *pcb = tcp_new();
|
||||
if (pcb == nullptr)
|
||||
return nullptr;
|
||||
auto *sock = new LWIPRawImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
// Create listening socket implementation since user sockets typically bind+listen
|
||||
// Accepted connections are created directly as LWIPRawImpl in the accept callback
|
||||
auto *sock = new LWIPRawListenImpl((sa_family_t) domain, pcb); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
sock->init();
|
||||
return std::unique_ptr<Socket>{sock};
|
||||
}
|
||||
|
||||
@@ -19,72 +19,54 @@ ListEntitiesIterator::~ListEntitiesIterator() {}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
bool ListEntitiesIterator::on_binary_sensor(binary_sensor::BinarySensor *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::binary_sensor_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool ListEntitiesIterator::on_cover(cover::Cover *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::cover_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool ListEntitiesIterator::on_fan(fan::Fan *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::fan_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool ListEntitiesIterator::on_light(light::LightState *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::light_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool ListEntitiesIterator::on_sensor(sensor::Sensor *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::sensor_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool ListEntitiesIterator::on_switch(switch_::Switch *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::switch_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
bool ListEntitiesIterator::on_button(button::Button *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::button_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool ListEntitiesIterator::on_text_sensor(text_sensor::TextSensor *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_sensor_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool ListEntitiesIterator::on_lock(lock::Lock *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::lock_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -92,8 +74,6 @@ bool ListEntitiesIterator::on_lock(lock::Lock *obj) {
|
||||
|
||||
#ifdef USE_VALVE
|
||||
bool ListEntitiesIterator::on_valve(valve::Valve *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::valve_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -101,8 +81,6 @@ bool ListEntitiesIterator::on_valve(valve::Valve *obj) {
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
bool ListEntitiesIterator::on_climate(climate::Climate *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::climate_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -110,8 +88,6 @@ bool ListEntitiesIterator::on_climate(climate::Climate *obj) {
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
bool ListEntitiesIterator::on_number(number::Number *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::number_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -119,8 +95,6 @@ bool ListEntitiesIterator::on_number(number::Number *obj) {
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::date_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -128,8 +102,6 @@ bool ListEntitiesIterator::on_date(datetime::DateEntity *obj) {
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::time_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -137,8 +109,6 @@ bool ListEntitiesIterator::on_time(datetime::TimeEntity *obj) {
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::datetime_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -146,8 +116,6 @@ bool ListEntitiesIterator::on_datetime(datetime::DateTimeEntity *obj) {
|
||||
|
||||
#ifdef USE_TEXT
|
||||
bool ListEntitiesIterator::on_text(text::Text *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::text_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -155,8 +123,6 @@ bool ListEntitiesIterator::on_text(text::Text *obj) {
|
||||
|
||||
#ifdef USE_SELECT
|
||||
bool ListEntitiesIterator::on_select(select::Select *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::select_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -164,8 +130,6 @@ bool ListEntitiesIterator::on_select(select::Select *obj) {
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::alarm_control_panel_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
@@ -173,8 +137,6 @@ bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmCont
|
||||
|
||||
#ifdef USE_EVENT
|
||||
bool ListEntitiesIterator::on_event(event::Event *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
// Null event type, since we are just iterating over entities
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::event_all_json_generator);
|
||||
return true;
|
||||
@@ -183,8 +145,6 @@ bool ListEntitiesIterator::on_event(event::Event *obj) {
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool ListEntitiesIterator::on_update(update::UpdateEntity *obj) {
|
||||
if (this->events_->count() == 0)
|
||||
return true;
|
||||
this->events_->deferrable_send_state(obj, "state_detail_all", WebServer::update_all_json_generator);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@ void DeferredUpdateEventSource::loop() {
|
||||
|
||||
void DeferredUpdateEventSource::deferrable_send_state(void *source, const char *event_type,
|
||||
message_generator_t *message_generator) {
|
||||
// Skip if no connected clients to avoid unnecessary deferred queue processing
|
||||
if (this->count() == 0)
|
||||
return;
|
||||
|
||||
// allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
|
||||
// up in the web GUI and reduces event load during initial connect
|
||||
if (!entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
|
||||
@@ -197,6 +201,9 @@ void DeferredUpdateEventSourceList::loop() {
|
||||
|
||||
void DeferredUpdateEventSourceList::deferrable_send_state(void *source, const char *event_type,
|
||||
message_generator_t *message_generator) {
|
||||
// Skip if no event sources (no connected clients) to avoid unnecessary iteration
|
||||
if (this->empty())
|
||||
return;
|
||||
for (DeferredUpdateEventSource *dues : *this) {
|
||||
dues->deferrable_send_state(source, event_type, message_generator);
|
||||
}
|
||||
@@ -424,8 +431,6 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
|
||||
|
||||
#ifdef USE_SENSOR
|
||||
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -453,13 +458,8 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
|
||||
|
||||
const auto uom_ref = obj->get_unit_of_measurement_ref();
|
||||
|
||||
// Build JSON directly inline
|
||||
std::string state;
|
||||
if (std::isnan(value)) {
|
||||
state = "NA";
|
||||
} else {
|
||||
state = value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
|
||||
}
|
||||
std::string state =
|
||||
std::isnan(value) ? "NA" : value_accuracy_with_uom_to_string(value, obj->get_accuracy_decimals(), uom_ref);
|
||||
set_json_icon_state_value(root, obj, "sensor", state, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
@@ -473,8 +473,6 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
|
||||
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -514,8 +512,6 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", switch_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -627,8 +623,6 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -667,8 +661,6 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
|
||||
|
||||
#ifdef USE_FAN
|
||||
void WebServer::on_fan_update(fan::Fan *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", fan_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -743,8 +735,6 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
void WebServer::on_light_update(light::LightState *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", light_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -800,8 +790,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "light", start_config);
|
||||
root["state"] = obj->remote_values.is_on() ? "ON" : "OFF";
|
||||
set_json_value(root, obj, "light", obj->remote_values.is_on() ? "ON" : "OFF", start_config);
|
||||
|
||||
light::LightJSONSchema::dump_json(*obj, root);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
@@ -819,8 +808,6 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
|
||||
|
||||
#ifdef USE_COVER
|
||||
void WebServer::on_cover_update(cover::Cover *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", cover_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -906,8 +893,6 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
void WebServer::on_number_update(number::Number *obj, float state) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", number_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -948,7 +933,13 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
|
||||
|
||||
const auto uom_ref = obj->traits.get_unit_of_measurement_ref();
|
||||
|
||||
set_json_id(root, obj, "number", start_config);
|
||||
std::string val_str = std::isnan(value)
|
||||
? "\"NaN\""
|
||||
: value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
|
||||
std::string state_str = std::isnan(value) ? "NA"
|
||||
: value_accuracy_with_uom_to_string(
|
||||
value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
|
||||
set_json_icon_state_value(root, obj, "number", state_str, val_str, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root["min_value"] =
|
||||
value_accuracy_to_string(obj->traits.get_min_value(), step_to_accuracy_decimals(obj->traits.get_step()));
|
||||
@@ -960,14 +951,6 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
|
||||
root["uom"] = uom_ref;
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
if (std::isnan(value)) {
|
||||
root["value"] = "\"NaN\"";
|
||||
root["state"] = "NA";
|
||||
} else {
|
||||
root["value"] = value_accuracy_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()));
|
||||
root["state"] =
|
||||
value_accuracy_with_uom_to_string(value, step_to_accuracy_decimals(obj->traits.get_step()), uom_ref);
|
||||
}
|
||||
|
||||
return builder.serialize();
|
||||
}
|
||||
@@ -975,8 +958,6 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void WebServer::on_date_update(datetime::DateEntity *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", date_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1020,10 +1001,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "date", start_config);
|
||||
std::string value = str_sprintf("%d-%02d-%02d", obj->year, obj->month, obj->day);
|
||||
root["value"] = value;
|
||||
root["state"] = value;
|
||||
set_json_icon_state_value(root, obj, "date", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1034,8 +1013,6 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void WebServer::on_time_update(datetime::TimeEntity *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", time_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1078,10 +1055,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "time", start_config);
|
||||
std::string value = str_sprintf("%02d:%02d:%02d", obj->hour, obj->minute, obj->second);
|
||||
root["value"] = value;
|
||||
root["state"] = value;
|
||||
set_json_icon_state_value(root, obj, "time", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1092,8 +1067,6 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1136,11 +1109,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "datetime", start_config);
|
||||
std::string value =
|
||||
str_sprintf("%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, obj->second);
|
||||
root["value"] = value;
|
||||
root["state"] = value;
|
||||
set_json_icon_state_value(root, obj, "datetime", value, value, start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
this->add_sorting_info_(root, obj);
|
||||
}
|
||||
@@ -1151,8 +1122,6 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
|
||||
|
||||
#ifdef USE_TEXT
|
||||
void WebServer::on_text_update(text::Text *obj, const std::string &state) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", text_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1191,16 +1160,11 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "text", start_config);
|
||||
std::string state = obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD ? "********" : value;
|
||||
set_json_icon_state_value(root, obj, "text", state, value, start_config);
|
||||
root["min_length"] = obj->traits.get_min_length();
|
||||
root["max_length"] = obj->traits.get_max_length();
|
||||
root["pattern"] = obj->traits.get_pattern();
|
||||
if (obj->traits.get_mode() == text::TextMode::TEXT_MODE_PASSWORD) {
|
||||
root["state"] = "********";
|
||||
} else {
|
||||
root["state"] = value;
|
||||
}
|
||||
root["value"] = value;
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root["mode"] = (int) obj->traits.get_mode();
|
||||
this->add_sorting_info_(root, obj);
|
||||
@@ -1212,8 +1176,6 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
|
||||
|
||||
#ifdef USE_SELECT
|
||||
void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", select_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1270,8 +1232,6 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
void WebServer::on_climate_update(climate::Climate *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", climate_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1412,8 +1372,6 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
|
||||
|
||||
#ifdef USE_LOCK
|
||||
void WebServer::on_lock_update(lock::Lock *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", lock_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1485,8 +1443,6 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet
|
||||
|
||||
#ifdef USE_VALVE
|
||||
void WebServer::on_valve_update(valve::Valve *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", valve_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1568,8 +1524,6 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1714,8 +1668,6 @@ static const char *update_state_to_string(update::UpdateState state) {
|
||||
}
|
||||
|
||||
void WebServer::on_update(update::UpdateEntity *obj) {
|
||||
if (this->events_.empty())
|
||||
return;
|
||||
this->events_.deferrable_send_state(obj, "state", update_state_json_generator);
|
||||
}
|
||||
void WebServer::handle_update_request(AsyncWebServerRequest *request, const UrlMatch &match) {
|
||||
@@ -1754,9 +1706,8 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c
|
||||
json::JsonBuilder builder;
|
||||
JsonObject root = builder.root();
|
||||
|
||||
set_json_id(root, obj, "update", start_config);
|
||||
root["value"] = obj->update_info.latest_version;
|
||||
root["state"] = update_state_to_string(obj->state);
|
||||
set_json_icon_state_value(root, obj, "update", update_state_to_string(obj->state), obj->update_info.latest_version,
|
||||
start_config);
|
||||
if (start_config == DETAIL_ALL) {
|
||||
root["current_version"] = obj->update_info.current_version;
|
||||
root["title"] = obj->update_info.title;
|
||||
|
||||
@@ -380,24 +380,25 @@ void AsyncEventSource::handleRequest(AsyncWebServerRequest *request) {
|
||||
if (this->on_connect_) {
|
||||
this->on_connect_(rsp);
|
||||
}
|
||||
this->sessions_.insert(rsp);
|
||||
this->sessions_.push_back(rsp);
|
||||
}
|
||||
|
||||
void AsyncEventSource::loop() {
|
||||
// Clean up dead sessions safely
|
||||
// This follows the ESP-IDF pattern where free_ctx marks resources as dead
|
||||
// and the main loop handles the actual cleanup to avoid race conditions
|
||||
auto it = this->sessions_.begin();
|
||||
while (it != this->sessions_.end()) {
|
||||
auto *ses = *it;
|
||||
for (size_t i = 0; i < this->sessions_.size();) {
|
||||
auto *ses = this->sessions_[i];
|
||||
// If the session has a dead socket (marked by destroy callback)
|
||||
if (ses->fd_.load() == 0) {
|
||||
ESP_LOGD(TAG, "Removing dead event source session");
|
||||
it = this->sessions_.erase(it);
|
||||
delete ses; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
// Remove by swapping with last element (O(1) removal, order doesn't matter for sessions)
|
||||
this->sessions_[i] = this->sessions_.back();
|
||||
this->sessions_.pop_back();
|
||||
} else {
|
||||
ses->loop();
|
||||
++it;
|
||||
++i;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -412,6 +413,9 @@ void AsyncEventSource::try_send_nodefer(const char *message, const char *event,
|
||||
|
||||
void AsyncEventSource::deferrable_send_state(void *source, const char *event_type,
|
||||
message_generator_t *message_generator) {
|
||||
// Skip if no connected clients to avoid unnecessary processing
|
||||
if (this->empty())
|
||||
return;
|
||||
for (auto *ses : this->sessions_) {
|
||||
if (ses->fd_.load() != 0) { // Skip dead sessions
|
||||
ses->deferrable_send_state(source, event_type, message_generator);
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#include <functional>
|
||||
#include <list>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -315,7 +314,10 @@ class AsyncEventSource : public AsyncWebHandler {
|
||||
|
||||
protected:
|
||||
std::string url_;
|
||||
std::set<AsyncEventSourceResponse *> sessions_;
|
||||
// Use vector instead of set: SSE sessions are typically 1-5 connections (browsers, dashboards).
|
||||
// Linear search is faster than red-black tree overhead for this small dataset.
|
||||
// Only operations needed: add session, remove session, iterate sessions - no need for sorted order.
|
||||
std::vector<AsyncEventSourceResponse *> sessions_;
|
||||
connect_handler_t on_connect_{};
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
};
|
||||
|
||||
@@ -447,6 +447,8 @@ async def to_code(config):
|
||||
var.get_disconnect_trigger(), [], on_disconnect_config
|
||||
)
|
||||
|
||||
CORE.add_job(final_step)
|
||||
|
||||
|
||||
@automation.register_condition("wifi.connected", WiFiConnectedCondition, cv.Schema({}))
|
||||
async def wifi_connected_to_code(config, condition_id, template_arg, args):
|
||||
@@ -468,6 +470,28 @@ async def wifi_disable_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
|
||||
KEEP_SCAN_RESULTS_KEY = "wifi_keep_scan_results"
|
||||
|
||||
|
||||
def request_wifi_scan_results():
|
||||
"""Request that WiFi scan results be kept in memory after connection.
|
||||
|
||||
Components that need access to scan results after WiFi is connected should
|
||||
call this function during their code generation. This prevents the WiFi component from
|
||||
freeing scan result memory after successful connection.
|
||||
"""
|
||||
CORE.data[KEEP_SCAN_RESULTS_KEY] = True
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.FINAL)
|
||||
async def final_step():
|
||||
"""Final code generation step to configure scan result retention."""
|
||||
if CORE.data.get(KEEP_SCAN_RESULTS_KEY, False):
|
||||
cg.add(
|
||||
cg.RawExpression("wifi::global_wifi_component->set_keep_scan_results(true)")
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"wifi.configure",
|
||||
WiFiConfigureAction,
|
||||
|
||||
@@ -265,12 +265,9 @@ network::IPAddress WiFiComponent::get_dns_address(int num) {
|
||||
return this->wifi_dns_ip_(num);
|
||||
return {};
|
||||
}
|
||||
std::string WiFiComponent::get_use_address() const {
|
||||
if (this->use_address_.empty()) {
|
||||
return App.get_name() + ".local";
|
||||
}
|
||||
return this->use_address_;
|
||||
}
|
||||
// set_use_address() is guaranteed to be called during component setup by Python code generation,
|
||||
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
|
||||
const std::string &WiFiComponent::get_use_address() const { return this->use_address_; }
|
||||
void WiFiComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
|
||||
|
||||
#ifdef USE_WIFI_AP
|
||||
@@ -552,7 +549,7 @@ void WiFiComponent::start_scanning() {
|
||||
// Using insertion sort instead of std::stable_sort saves flash memory
|
||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
|
||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
|
||||
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
|
||||
template<typename VectorType> static void insertion_sort_scan_results(VectorType &results) {
|
||||
const size_t size = results.size();
|
||||
for (size_t i = 1; i < size; i++) {
|
||||
// Make a copy to avoid issues with move semantics during comparison
|
||||
@@ -716,6 +713,12 @@ void WiFiComponent::check_connecting_finished() {
|
||||
this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTED;
|
||||
this->num_retried_ = 0;
|
||||
|
||||
// Free scan results memory unless a component needs them
|
||||
if (!this->keep_scan_results_) {
|
||||
this->scan_result_.clear();
|
||||
this->scan_result_.shrink_to_fit();
|
||||
}
|
||||
|
||||
if (this->fast_connect_) {
|
||||
this->save_fast_connect_settings_();
|
||||
}
|
||||
|
||||
@@ -121,6 +121,14 @@ struct EAPAuth {
|
||||
|
||||
using bssid_t = std::array<uint8_t, 6>;
|
||||
|
||||
// Use std::vector for RP2040 since scan count is unknown (callback-based)
|
||||
// Use FixedVector for other platforms where count is queried first
|
||||
#ifdef USE_RP2040
|
||||
template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||
#else
|
||||
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
#endif
|
||||
|
||||
class WiFiAP {
|
||||
public:
|
||||
void set_ssid(const std::string &ssid);
|
||||
@@ -275,10 +283,10 @@ class WiFiComponent : public Component {
|
||||
|
||||
network::IPAddress get_dns_address(int num);
|
||||
network::IPAddresses get_ip_addresses();
|
||||
std::string get_use_address() const;
|
||||
const std::string &get_use_address() const;
|
||||
void set_use_address(const std::string &use_address);
|
||||
|
||||
const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||
const wifi_scan_vector_t<WiFiScanResult> &get_scan_result() const { return scan_result_; }
|
||||
|
||||
network::IPAddress wifi_soft_ap_ip();
|
||||
|
||||
@@ -316,6 +324,7 @@ class WiFiComponent : public Component {
|
||||
int8_t wifi_rssi();
|
||||
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; }
|
||||
|
||||
Trigger<> *get_connect_trigger() const { return this->connect_trigger_; };
|
||||
Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; };
|
||||
@@ -385,7 +394,7 @@ class WiFiComponent : public Component {
|
||||
std::string use_address_;
|
||||
std::vector<WiFiAP> sta_;
|
||||
std::vector<WiFiSTAPriority> sta_priorities_;
|
||||
std::vector<WiFiScanResult> scan_result_;
|
||||
wifi_scan_vector_t<WiFiScanResult> scan_result_;
|
||||
WiFiAP selected_ap_;
|
||||
WiFiAP ap_;
|
||||
optional<float> output_power_;
|
||||
@@ -424,6 +433,7 @@ class WiFiComponent : public Component {
|
||||
#endif
|
||||
bool enable_on_boot_;
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
|
||||
// Pointers at the end (naturally aligned)
|
||||
Trigger<> *connect_trigger_{new Trigger<>()};
|
||||
|
||||
@@ -696,7 +696,15 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
this->retry_connect();
|
||||
return;
|
||||
}
|
||||
|
||||
// Count the number of results first
|
||||
auto *head = reinterpret_cast<bss_info *>(arg);
|
||||
size_t count = 0;
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
count++;
|
||||
}
|
||||
|
||||
this->scan_result_.init(count);
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
|
||||
|
||||
@@ -784,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
records.resize(number);
|
||||
|
||||
scan_result_.reserve(number);
|
||||
scan_result_.init(number);
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto &record = records[i];
|
||||
bssid_t bssid;
|
||||
|
||||
@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
if (num < 0)
|
||||
return;
|
||||
|
||||
this->scan_result_.reserve(static_cast<unsigned int>(num));
|
||||
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.components import text_sensor, wifi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BSSID,
|
||||
@@ -77,7 +77,9 @@ async def to_code(config):
|
||||
await setup_conf(config, CONF_SSID)
|
||||
await setup_conf(config, CONF_BSSID)
|
||||
await setup_conf(config, CONF_MAC_ADDRESS)
|
||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||
if CONF_SCAN_RESULTS in config:
|
||||
await setup_conf(config, CONF_SCAN_RESULTS)
|
||||
wifi.request_wifi_scan_results()
|
||||
await setup_conf(config, CONF_DNS_ADDRESS)
|
||||
if conf := config.get(CONF_IP_ADDRESS):
|
||||
wifi_info = await text_sensor.new_text_sensor(config[CONF_IP_ADDRESS])
|
||||
|
||||
@@ -1195,6 +1195,13 @@ def validate_bytes(value):
|
||||
|
||||
|
||||
def hostname(value):
|
||||
"""Validate that the value is a valid hostname.
|
||||
|
||||
Maximum length is 63 characters per RFC 1035.
|
||||
|
||||
Note: If this limit is changed, update MAX_NAME_WITH_SUFFIX_SIZE in
|
||||
esphome/core/helpers.cpp to accommodate the new maximum length.
|
||||
"""
|
||||
value = string(value)
|
||||
if re.match(r"^[a-z0-9-]{1,63}$", value, re.IGNORECASE) is not None:
|
||||
return value
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.10.0"
|
||||
__version__ = "2025.11.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
@@ -836,6 +836,7 @@ CONF_RMT_CHANNEL = "rmt_channel"
|
||||
CONF_RMT_SYMBOLS = "rmt_symbols"
|
||||
CONF_ROTATION = "rotation"
|
||||
CONF_ROW = "row"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_RS_PIN = "rs_pin"
|
||||
CONF_RTD_NOMINAL_RESISTANCE = "rtd_nominal_resistance"
|
||||
CONF_RTD_WIRES = "rtd_wires"
|
||||
|
||||
@@ -340,8 +340,8 @@ void Application::calculate_looping_components_() {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-reserve vector to avoid reallocations
|
||||
this->looping_components_.reserve(total_looping);
|
||||
// Initialize FixedVector with exact size - no reallocation possible
|
||||
this->looping_components_.init(total_looping);
|
||||
|
||||
// Add all components with loop override that aren't already LOOP_DONE
|
||||
// Some components (like logger) may call disable_loop() during initialization
|
||||
|
||||
@@ -102,9 +102,15 @@ class Application {
|
||||
arch_init();
|
||||
this->name_add_mac_suffix_ = name_add_mac_suffix;
|
||||
if (name_add_mac_suffix) {
|
||||
const std::string mac_suffix = get_mac_address().substr(6);
|
||||
this->name_ = name + "-" + mac_suffix;
|
||||
this->friendly_name_ = friendly_name.empty() ? "" : friendly_name + " " + mac_suffix;
|
||||
// MAC address suffix length (last 6 characters of 12-char MAC address string)
|
||||
constexpr size_t mac_address_suffix_len = 6;
|
||||
const std::string mac_addr = get_mac_address();
|
||||
// Use pointer + offset to avoid substr() allocation
|
||||
const char *mac_suffix_ptr = mac_addr.c_str() + mac_address_suffix_len;
|
||||
this->name_ = make_name_with_suffix(name, '-', mac_suffix_ptr, mac_address_suffix_len);
|
||||
if (!friendly_name.empty()) {
|
||||
this->friendly_name_ = make_name_with_suffix(friendly_name, ' ', mac_suffix_ptr, mac_address_suffix_len);
|
||||
}
|
||||
} else {
|
||||
this->name_ = name;
|
||||
this->friendly_name_ = friendly_name;
|
||||
@@ -472,7 +478,7 @@ class Application {
|
||||
// - When a component is enabled, it's swapped with the first inactive component
|
||||
// and active_end_ is incremented
|
||||
// - This eliminates branch mispredictions from flag checking in the hot loop
|
||||
std::vector<Component *> looping_components_{};
|
||||
FixedVector<Component *> looping_components_{};
|
||||
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||
std::vector<int> socket_fds_; // Vector of all monitored socket file descriptors
|
||||
#endif
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
@@ -14,7 +15,7 @@ namespace esphome {
|
||||
|
||||
template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit AndCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit AndCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
for (auto *condition : this->conditions_) {
|
||||
if (!condition->check(x...))
|
||||
@@ -25,12 +26,12 @@ template<typename... Ts> class AndCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit OrCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit OrCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
for (auto *condition : this->conditions_) {
|
||||
if (condition->check(x...))
|
||||
@@ -41,7 +42,7 @@ template<typename... Ts> class OrCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
||||
@@ -55,7 +56,7 @@ template<typename... Ts> class NotCondition : public Condition<Ts...> {
|
||||
|
||||
template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit XorCondition(const std::vector<Condition<Ts...> *> &conditions) : conditions_(conditions) {}
|
||||
explicit XorCondition(std::initializer_list<Condition<Ts...> *> conditions) : conditions_(conditions) {}
|
||||
bool check(Ts... x) override {
|
||||
size_t result = 0;
|
||||
for (auto *condition : this->conditions_) {
|
||||
@@ -66,7 +67,7 @@ template<typename... Ts> class XorCondition : public Condition<Ts...> {
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<Condition<Ts...> *> conditions_;
|
||||
FixedVector<Condition<Ts...> *> conditions_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class LambdaCondition : public Condition<Ts...> {
|
||||
|
||||
@@ -200,7 +200,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.valid_name,
|
||||
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.string,
|
||||
cv.Optional(CONF_FRIENDLY_NAME, ""): cv.All(cv.string, cv.Length(max=120)),
|
||||
cv.Optional(CONF_AREA): validate_area_config,
|
||||
cv.Optional(CONF_COMMENT): cv.string,
|
||||
cv.Required(CONF_BUILD_PATH): cv.string,
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
#define USE_LVGL_TILEVIEW
|
||||
#define USE_LVGL_TOUCHSCREEN
|
||||
#define USE_MDNS
|
||||
#define USE_MDNS_STORE_SERVICES
|
||||
#define MDNS_SERVICE_COUNT 3
|
||||
#define MDNS_DYNAMIC_TXT_COUNT 3
|
||||
#define USE_MEDIA_PLAYER
|
||||
@@ -175,6 +176,13 @@
|
||||
#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE
|
||||
#define USE_ESP32_BLE_SERVER_ON_CONNECT
|
||||
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT
|
||||
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT 2
|
||||
#define ESPHOME_ESP32_BLE_GAP_SCAN_EVENT_HANDLER_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_GATTC_EVENT_HANDLER_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT 1
|
||||
#define ESPHOME_ESP32_BLE_BLE_STATUS_EVENT_HANDLER_COUNT 2
|
||||
#define USE_ESP32_CAMERA_JPEG_ENCODER
|
||||
#define USE_I2C
|
||||
#define USE_IMPROV
|
||||
@@ -194,7 +202,7 @@
|
||||
#define USB_HOST_MAX_REQUESTS 16
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
|
||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
|
||||
#define USE_ETHERNET
|
||||
#define USE_ETHERNET_KSZ8081
|
||||
#endif
|
||||
|
||||
@@ -235,6 +235,30 @@ std::string str_sprintf(const char *fmt, ...) {
|
||||
return str;
|
||||
}
|
||||
|
||||
// Maximum size for name with suffix: 120 (max friendly name) + 1 (separator) + 6 (MAC suffix) + 1 (null term)
|
||||
static constexpr size_t MAX_NAME_WITH_SUFFIX_SIZE = 128;
|
||||
|
||||
std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len) {
|
||||
char buffer[MAX_NAME_WITH_SUFFIX_SIZE];
|
||||
size_t name_len = name.size();
|
||||
size_t total_len = name_len + 1 + suffix_len;
|
||||
|
||||
// Silently truncate if needed: prioritize keeping the full suffix
|
||||
if (total_len >= MAX_NAME_WITH_SUFFIX_SIZE) {
|
||||
// NOTE: This calculation could underflow if suffix_len >= MAX_NAME_WITH_SUFFIX_SIZE - 2,
|
||||
// but this is safe because this helper is only called with small suffixes:
|
||||
// MAC suffixes (6-12 bytes), ".local" (5 bytes), etc.
|
||||
name_len = MAX_NAME_WITH_SUFFIX_SIZE - suffix_len - 2; // -2 for separator and null terminator
|
||||
total_len = name_len + 1 + suffix_len;
|
||||
}
|
||||
|
||||
memcpy(buffer, name.c_str(), name_len);
|
||||
buffer[name_len] = sep;
|
||||
memcpy(buffer + name_len + 1, suffix_ptr, suffix_len);
|
||||
buffer[total_len] = '\0';
|
||||
return std::string(buffer, total_len);
|
||||
}
|
||||
|
||||
// Parsing & formatting
|
||||
|
||||
size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) {
|
||||
|
||||
@@ -159,6 +159,159 @@ template<typename T, size_t N> class StaticVector {
|
||||
const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
|
||||
};
|
||||
|
||||
/// Fixed-capacity vector - allocates once at runtime, never reallocates
|
||||
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
|
||||
/// when size is known at initialization but not at compile time
|
||||
template<typename T> class FixedVector {
|
||||
private:
|
||||
T *data_{nullptr};
|
||||
size_t size_{0};
|
||||
size_t capacity_{0};
|
||||
|
||||
// Helper to destroy all elements without freeing memory
|
||||
void destroy_elements_() {
|
||||
// Only call destructors for non-trivially destructible types
|
||||
if constexpr (!std::is_trivially_destructible<T>::value) {
|
||||
for (size_t i = 0; i < size_; i++) {
|
||||
data_[i].~T();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to destroy elements and free memory
|
||||
void cleanup_() {
|
||||
if (data_ != nullptr) {
|
||||
destroy_elements_();
|
||||
// Free raw memory
|
||||
::operator delete(data_);
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to reset pointers after cleanup
|
||||
void reset_() {
|
||||
data_ = nullptr;
|
||||
capacity_ = 0;
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
public:
|
||||
FixedVector() = default;
|
||||
|
||||
/// Constructor from initializer list - allocates exact size needed
|
||||
/// This enables brace initialization: FixedVector<int> v = {1, 2, 3};
|
||||
FixedVector(std::initializer_list<T> init_list) {
|
||||
init(init_list.size());
|
||||
size_t idx = 0;
|
||||
for (const auto &item : init_list) {
|
||||
new (data_ + idx) T(item);
|
||||
++idx;
|
||||
}
|
||||
size_ = init_list.size();
|
||||
}
|
||||
|
||||
~FixedVector() { cleanup_(); }
|
||||
|
||||
// Disable copy operations (avoid accidental expensive copies)
|
||||
FixedVector(const FixedVector &) = delete;
|
||||
FixedVector &operator=(const FixedVector &) = delete;
|
||||
|
||||
// Enable move semantics (allows use in move-only containers like std::vector)
|
||||
FixedVector(FixedVector &&other) noexcept : data_(other.data_), size_(other.size_), capacity_(other.capacity_) {
|
||||
other.reset_();
|
||||
}
|
||||
|
||||
FixedVector &operator=(FixedVector &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// Delete our current data
|
||||
cleanup_();
|
||||
// Take ownership of other's data
|
||||
data_ = other.data_;
|
||||
size_ = other.size_;
|
||||
capacity_ = other.capacity_;
|
||||
// Leave other in valid empty state
|
||||
other.reset_();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Allocate capacity - can be called multiple times to reinit
|
||||
void init(size_t n) {
|
||||
cleanup_();
|
||||
reset_();
|
||||
if (n > 0) {
|
||||
// Allocate raw memory without calling constructors
|
||||
// sizeof(T) is correct here for any type T (value types, pointers, etc.)
|
||||
// NOLINTNEXTLINE(bugprone-sizeof-expression)
|
||||
data_ = static_cast<T *>(::operator new(n * sizeof(T)));
|
||||
capacity_ = n;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the vector (destroy all elements, reset size to 0, keep capacity)
|
||||
void clear() {
|
||||
destroy_elements_();
|
||||
size_ = 0;
|
||||
}
|
||||
|
||||
// Shrink capacity to fit current size (frees all memory)
|
||||
void shrink_to_fit() {
|
||||
cleanup_();
|
||||
reset_();
|
||||
}
|
||||
|
||||
/// Add element without bounds checking
|
||||
/// Caller must ensure sufficient capacity was allocated via init()
|
||||
/// Silently ignores pushes beyond capacity (no exception or assertion)
|
||||
void push_back(const T &value) {
|
||||
if (size_ < capacity_) {
|
||||
// Use placement new to construct the object in pre-allocated memory
|
||||
new (&data_[size_]) T(value);
|
||||
size_++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Add element by move without bounds checking
|
||||
/// Caller must ensure sufficient capacity was allocated via init()
|
||||
/// Silently ignores pushes beyond capacity (no exception or assertion)
|
||||
void push_back(T &&value) {
|
||||
if (size_ < capacity_) {
|
||||
// Use placement new to move-construct the object in pre-allocated memory
|
||||
new (&data_[size_]) T(std::move(value));
|
||||
size_++;
|
||||
}
|
||||
}
|
||||
|
||||
/// Emplace element without bounds checking - constructs in-place
|
||||
/// Caller must ensure sufficient capacity was allocated via init()
|
||||
/// Returns reference to the newly constructed element
|
||||
/// NOTE: Caller MUST ensure size_ < capacity_ before calling
|
||||
T &emplace_back() {
|
||||
// Use placement new to default-construct the object in pre-allocated memory
|
||||
new (&data_[size_]) T();
|
||||
size_++;
|
||||
return data_[size_ - 1];
|
||||
}
|
||||
|
||||
/// Access last element (no bounds checking - matches std::vector behavior)
|
||||
/// Caller must ensure vector is not empty (size() > 0)
|
||||
T &back() { return data_[size_ - 1]; }
|
||||
const T &back() const { return data_[size_ - 1]; }
|
||||
|
||||
size_t size() const { return size_; }
|
||||
bool empty() const { return size_ == 0; }
|
||||
|
||||
/// Access element without bounds checking (matches std::vector behavior)
|
||||
/// Caller must ensure index is valid (i < size())
|
||||
T &operator[](size_t i) { return data_[i]; }
|
||||
const T &operator[](size_t i) const { return data_[i]; }
|
||||
|
||||
// Iterator support for range-based for loops
|
||||
T *begin() { return data_; }
|
||||
T *end() { return data_ + size_; }
|
||||
const T *begin() const { return data_; }
|
||||
const T *end() const { return data_ + size_; }
|
||||
};
|
||||
|
||||
///@}
|
||||
|
||||
/// @name Mathematics
|
||||
@@ -306,6 +459,16 @@ std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt,
|
||||
/// sprintf-like function returning std::string.
|
||||
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
|
||||
|
||||
/// Concatenate a name with a separator and suffix using an efficient stack-based approach.
|
||||
/// This avoids multiple heap allocations during string construction.
|
||||
/// Maximum name length supported is 120 characters for friendly names.
|
||||
/// @param name The base name string
|
||||
/// @param sep The separator character (e.g., '-', ' ', or '.')
|
||||
/// @param suffix_ptr Pointer to the suffix characters
|
||||
/// @param suffix_len Length of the suffix
|
||||
/// @return The concatenated string: name + sep + suffix
|
||||
std::string make_name_with_suffix(const std::string &name, char sep, const char *suffix_ptr, size_t suffix_len);
|
||||
|
||||
///@}
|
||||
|
||||
/// @name Parsing & formatting
|
||||
|
||||
@@ -125,9 +125,9 @@ extra_scripts = post:esphome/components/esp8266/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using Arduino.
|
||||
[common:esp32-arduino]
|
||||
extends = common:arduino
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.2.1/esp32-3.2.1.zip
|
||||
pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/3.3.2/esp32-3.3.2.zip
|
||||
|
||||
framework = arduino, espidf ; Arduino as an ESP-IDF component
|
||||
lib_deps =
|
||||
@@ -161,9 +161,9 @@ extra_scripts = post:esphome/components/esp32/post_build.py.script
|
||||
; This are common settings for the ESP32 (all variants) using IDF.
|
||||
[common:esp32-idf]
|
||||
extends = common:idf
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/54.03.21-2/platform-espressif32.zip
|
||||
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.31-1/platform-espressif32.zip
|
||||
platform_packages =
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.4.2/esp-idf-v5.4.2.zip
|
||||
pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v5.5.1/esp-idf-v5.5.1.zip
|
||||
|
||||
framework = espidf
|
||||
lib_deps =
|
||||
|
||||
@@ -10,23 +10,19 @@ tzdata>=2021.1 # from time
|
||||
pyserial==3.5
|
||||
platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==5.1.0
|
||||
click==8.1.7
|
||||
click==8.3.0
|
||||
esphome-dashboard==20251013.0
|
||||
aioesphomeapi==41.16.1
|
||||
aioesphomeapi==42.0.0
|
||||
zeroconf==0.148.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.15 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.12 # dashboard_import
|
||||
ruamel.yaml.clib==0.2.14 # dashboard_import
|
||||
esphome-glyphsets==0.2.0
|
||||
pillow==10.4.0
|
||||
pillow==11.3.0
|
||||
cairosvg==2.8.2
|
||||
freetype-py==2.5.1
|
||||
jinja2==3.1.6
|
||||
|
||||
# esp-idf requires this, but doesn't bundle it by default
|
||||
# https://github.com/espressif/esp-idf/blob/220590d599e134d7a5e7f1e683cc4550349ffbf8/requirements.txt#L24
|
||||
kconfiglib==13.7.1
|
||||
|
||||
# esp-idf >= 5.0 requires this
|
||||
pyparsing >= 3.0
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pylint==3.3.9
|
||||
pylint==4.0.1
|
||||
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
|
||||
ruff==0.14.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.20.0 # also change in .pre-commit-config.yaml when updating
|
||||
pyupgrade==3.21.0 # also change in .pre-commit-config.yaml when updating
|
||||
pre-commit
|
||||
|
||||
# Unit tests
|
||||
|
||||
@@ -75,6 +75,8 @@ ISOLATED_COMPONENTS = {
|
||||
"ethernet": "Defines ethernet: which conflicts with wifi: used by most components",
|
||||
"ethernet_info": "Related to ethernet component which conflicts with wifi",
|
||||
"lvgl": "Defines multiple SDL displays on host platform that conflict when merged with other display configs",
|
||||
"openthread": "Conflicts with wifi: used by most components",
|
||||
"openthread_info": "Conflicts with wifi: used by most components",
|
||||
"matrix_keypad": "Needs isolation due to keypad",
|
||||
"mcp4725": "no YAML config to specify i2c bus id",
|
||||
"mcp47a1": "no YAML config to specify i2c bus id",
|
||||
|
||||
@@ -1415,6 +1415,8 @@ class RepeatedTypeInfo(TypeInfo):
|
||||
# Check if this is a pointer field by looking for container_pointer option
|
||||
self._container_type = get_field_opt(field, pb.container_pointer, "")
|
||||
self._use_pointer = bool(self._container_type)
|
||||
# Check if this should use FixedVector instead of std::vector
|
||||
self._use_fixed_vector = get_field_opt(field, pb.fixed_vector, False)
|
||||
|
||||
# For repeated fields, we need to get the base type info
|
||||
# but we can't call create_field_type_info as it would cause recursion
|
||||
@@ -1438,6 +1440,8 @@ class RepeatedTypeInfo(TypeInfo):
|
||||
if "<" in self._container_type and ">" in self._container_type:
|
||||
return f"const {self._container_type}*"
|
||||
return f"const {self._container_type}<{self._ti.cpp_type}>*"
|
||||
if self._use_fixed_vector:
|
||||
return f"FixedVector<{self._ti.cpp_type}>"
|
||||
return f"std::vector<{self._ti.cpp_type}>"
|
||||
|
||||
@property
|
||||
|
||||
@@ -501,7 +501,7 @@ def lint_constants_usage():
|
||||
continue
|
||||
errs.append(
|
||||
f"Constant {highlight(constant)} is defined in {len(uses)} files. Please move all definitions of the "
|
||||
f"constant to const.py (Uses: {', '.join(uses)}) in a separate PR. "
|
||||
f"constant to const.py (Uses: {', '.join(str(u) for u in uses)}) in a separate PR. "
|
||||
"See https://developers.esphome.io/contributing/code/#python"
|
||||
)
|
||||
return errs
|
||||
|
||||
@@ -31,6 +31,7 @@ Options:
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from functools import cache
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
@@ -45,7 +46,6 @@ from helpers import (
|
||||
changed_files,
|
||||
get_all_dependencies,
|
||||
get_components_from_integration_fixtures,
|
||||
parse_list_components_output,
|
||||
root_path,
|
||||
)
|
||||
|
||||
@@ -212,6 +212,24 @@ def _any_changed_file_endswith(branch: str | None, extensions: tuple[str, ...])
|
||||
return any(file.endswith(extensions) for file in changed_files(branch))
|
||||
|
||||
|
||||
@cache
|
||||
def _component_has_tests(component: str) -> bool:
|
||||
"""Check if a component has test files.
|
||||
|
||||
Cached to avoid repeated filesystem operations for the same component.
|
||||
|
||||
Args:
|
||||
component: Component name to check
|
||||
|
||||
Returns:
|
||||
True if the component has test YAML files
|
||||
"""
|
||||
tests_dir = Path(root_path) / "tests" / "components" / component
|
||||
if not tests_dir.exists():
|
||||
return False
|
||||
return any(tests_dir.glob("test.*.yaml"))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main function that determines which CI jobs to run."""
|
||||
parser = argparse.ArgumentParser(
|
||||
@@ -228,23 +246,37 @@ def main() -> None:
|
||||
run_clang_format = should_run_clang_format(args.branch)
|
||||
run_python_linters = should_run_python_linters(args.branch)
|
||||
|
||||
# Get changed components using list-components.py for exact compatibility
|
||||
# Get both directly changed and all changed components (with dependencies) in one call
|
||||
script_path = Path(__file__).parent / "list-components.py"
|
||||
cmd = [sys.executable, str(script_path), "--changed"]
|
||||
cmd = [sys.executable, str(script_path), "--changed-with-deps"]
|
||||
if args.branch:
|
||||
cmd.extend(["-b", args.branch])
|
||||
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
changed_components = parse_list_components_output(result.stdout)
|
||||
component_data = json.loads(result.stdout)
|
||||
directly_changed_components = component_data["directly_changed"]
|
||||
changed_components = component_data["all_changed"]
|
||||
|
||||
# Filter to only components that have test files
|
||||
# Components without tests shouldn't generate CI test jobs
|
||||
tests_dir = Path(root_path) / "tests" / "components"
|
||||
changed_components_with_tests = [
|
||||
component for component in changed_components if _component_has_tests(component)
|
||||
]
|
||||
|
||||
# Get directly changed components with tests (for isolated testing)
|
||||
# These will be tested WITHOUT --testing-mode in CI to enable full validation
|
||||
# (pin conflicts, etc.) since they contain the actual changes being reviewed
|
||||
directly_changed_with_tests = [
|
||||
component
|
||||
for component in changed_components
|
||||
if (component_test_dir := tests_dir / component).exists()
|
||||
and any(component_test_dir.glob("test.*.yaml"))
|
||||
for component in directly_changed_components
|
||||
if _component_has_tests(component)
|
||||
]
|
||||
|
||||
# Get dependency-only components (for grouped testing)
|
||||
dependency_only_components = [
|
||||
component
|
||||
for component in changed_components_with_tests
|
||||
if component not in directly_changed_components
|
||||
]
|
||||
|
||||
# Build output
|
||||
@@ -255,7 +287,11 @@ def main() -> None:
|
||||
"python_linters": run_python_linters,
|
||||
"changed_components": changed_components,
|
||||
"changed_components_with_tests": changed_components_with_tests,
|
||||
"directly_changed_components_with_tests": directly_changed_with_tests,
|
||||
"dependency_only_components_with_tests": dependency_only_components,
|
||||
"component_test_count": len(changed_components_with_tests),
|
||||
"directly_changed_count": len(directly_changed_with_tests),
|
||||
"dependency_only_count": len(dependency_only_components),
|
||||
}
|
||||
|
||||
# Output as JSON
|
||||
|
||||
@@ -185,18 +185,32 @@ def main():
|
||||
"-c",
|
||||
"--changed",
|
||||
action="store_true",
|
||||
help="List all components required for testing based on changes",
|
||||
help="List all components required for testing based on changes (includes dependencies)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--changed-direct",
|
||||
action="store_true",
|
||||
help="List only directly changed components (without dependencies)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--changed-with-deps",
|
||||
action="store_true",
|
||||
help="Output JSON with both directly changed and all changed components",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b", "--branch", help="Branch to compare changed files against"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.branch and not args.changed:
|
||||
parser.error("--branch requires --changed")
|
||||
if args.branch and not (
|
||||
args.changed or args.changed_direct or args.changed_with_deps
|
||||
):
|
||||
parser.error(
|
||||
"--branch requires --changed, --changed-direct, or --changed-with-deps"
|
||||
)
|
||||
|
||||
if args.changed:
|
||||
# When --changed is passed, only get the changed files
|
||||
if args.changed or args.changed_direct or args.changed_with_deps:
|
||||
# When --changed* is passed, only get the changed files
|
||||
changed = changed_files(args.branch)
|
||||
|
||||
# If any base test file(s) changed, there's no need to filter out components
|
||||
@@ -210,8 +224,25 @@ def main():
|
||||
# Get all component files
|
||||
files = get_all_component_files()
|
||||
|
||||
for c in get_components(files, args.changed):
|
||||
print(c)
|
||||
if args.changed_with_deps:
|
||||
# Return JSON with both directly changed and all changed components
|
||||
import json
|
||||
|
||||
directly_changed = get_components(files, False)
|
||||
all_changed = get_components(files, True)
|
||||
output = {
|
||||
"directly_changed": directly_changed,
|
||||
"all_changed": all_changed,
|
||||
}
|
||||
print(json.dumps(output))
|
||||
elif args.changed_direct:
|
||||
# Return only directly changed components (without dependencies)
|
||||
for c in get_components(files, False):
|
||||
print(c)
|
||||
else:
|
||||
# Return all changed components (with dependencies) - default behavior
|
||||
for c in get_components(files, args.changed):
|
||||
print(c)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -56,6 +56,7 @@ def create_intelligent_batches(
|
||||
components: list[str],
|
||||
tests_dir: Path,
|
||||
batch_size: int = 40,
|
||||
directly_changed: set[str] | None = None,
|
||||
) -> list[list[str]]:
|
||||
"""Create batches optimized for component grouping.
|
||||
|
||||
@@ -63,6 +64,7 @@ def create_intelligent_batches(
|
||||
components: List of component names to batch
|
||||
tests_dir: Path to tests/components directory
|
||||
batch_size: Target size for each batch
|
||||
directly_changed: Set of directly changed components (for logging only)
|
||||
|
||||
Returns:
|
||||
List of component batches (lists of component names)
|
||||
@@ -94,10 +96,17 @@ def create_intelligent_batches(
|
||||
|
||||
for component in components_with_tests:
|
||||
# Components that can't be grouped get unique signatures
|
||||
# This includes both manually curated ISOLATED_COMPONENTS and
|
||||
# automatically detected non_groupable components
|
||||
# This includes:
|
||||
# - Manually curated ISOLATED_COMPONENTS
|
||||
# - Automatically detected non_groupable components
|
||||
# - Directly changed components (passed via --isolate in CI)
|
||||
# These can share a batch/runner but won't be grouped/merged
|
||||
if component in ISOLATED_COMPONENTS or component in non_groupable:
|
||||
is_isolated = (
|
||||
component in ISOLATED_COMPONENTS
|
||||
or component in non_groupable
|
||||
or (directly_changed and component in directly_changed)
|
||||
)
|
||||
if is_isolated:
|
||||
signature_groups[f"isolated_{component}"].append(component)
|
||||
continue
|
||||
|
||||
@@ -187,6 +196,10 @@ def main() -> int:
|
||||
default=Path("tests/components"),
|
||||
help="Path to tests/components directory",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--directly-changed",
|
||||
help="JSON array of directly changed component names (for logging only)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output",
|
||||
"-o",
|
||||
@@ -208,11 +221,21 @@ def main() -> int:
|
||||
print("Components must be a JSON array", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Parse directly changed components list from JSON (if provided)
|
||||
directly_changed = None
|
||||
if args.directly_changed:
|
||||
try:
|
||||
directly_changed = set(json.loads(args.directly_changed))
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error parsing directly-changed JSON: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
# Create intelligent batches
|
||||
batches = create_intelligent_batches(
|
||||
components=components,
|
||||
tests_dir=args.tests_dir,
|
||||
batch_size=args.batch_size,
|
||||
directly_changed=directly_changed,
|
||||
)
|
||||
|
||||
# Convert batches to space-separated strings for CI
|
||||
@@ -238,13 +261,37 @@ def main() -> int:
|
||||
isolated_count = sum(
|
||||
1
|
||||
for comp in all_batched_components
|
||||
if comp in ISOLATED_COMPONENTS or comp in non_groupable
|
||||
if comp in ISOLATED_COMPONENTS
|
||||
or comp in non_groupable
|
||||
or (directly_changed and comp in directly_changed)
|
||||
)
|
||||
groupable_count = actual_components - isolated_count
|
||||
|
||||
print("\n=== Intelligent Batch Summary ===", file=sys.stderr)
|
||||
print(f"Total components requested: {len(components)}", file=sys.stderr)
|
||||
print(f"Components with test files: {actual_components}", file=sys.stderr)
|
||||
|
||||
# Show breakdown of directly changed vs dependencies
|
||||
if directly_changed:
|
||||
direct_count = sum(
|
||||
1 for comp in all_batched_components if comp in directly_changed
|
||||
)
|
||||
dep_count = actual_components - direct_count
|
||||
direct_comps = [
|
||||
comp for comp in all_batched_components if comp in directly_changed
|
||||
]
|
||||
dep_comps = [
|
||||
comp for comp in all_batched_components if comp not in directly_changed
|
||||
]
|
||||
print(
|
||||
f" - Direct changes: {direct_count} ({', '.join(sorted(direct_comps))})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print(
|
||||
f" - Dependencies: {dep_count} ({', '.join(sorted(dep_comps))})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
print(f" - Groupable (weight=1): {groupable_count}", file=sys.stderr)
|
||||
print(f" - Isolated (weight=10): {isolated_count}", file=sys.stderr)
|
||||
if actual_components < len(components):
|
||||
|
||||
@@ -365,6 +365,7 @@ def run_grouped_component_tests(
|
||||
build_dir: Path,
|
||||
esphome_command: str,
|
||||
continue_on_fail: bool,
|
||||
additional_isolated: set[str] | None = None,
|
||||
) -> tuple[set[tuple[str, str]], list[str], list[str], dict[str, str]]:
|
||||
"""Run grouped component tests.
|
||||
|
||||
@@ -376,6 +377,7 @@ def run_grouped_component_tests(
|
||||
build_dir: Path to build directory
|
||||
esphome_command: ESPHome command (config/compile)
|
||||
continue_on_fail: Whether to continue on failure
|
||||
additional_isolated: Additional components to treat as isolated (not grouped)
|
||||
|
||||
Returns:
|
||||
Tuple of (tested_components, passed_tests, failed_tests, failed_commands)
|
||||
@@ -397,6 +399,17 @@ def run_grouped_component_tests(
|
||||
# Track why components can't be grouped (for detailed output)
|
||||
non_groupable_reasons = {}
|
||||
|
||||
# Merge additional isolated components with predefined ones
|
||||
# ISOLATED COMPONENTS are tested individually WITHOUT --testing-mode
|
||||
# This is critical because:
|
||||
# - Grouped tests use --testing-mode which disables pin conflict checks and other validation
|
||||
# - These checks are disabled to allow config merging (multiple components in one build)
|
||||
# - For directly changed components (via --isolate), we need full validation to catch issues
|
||||
# - Dependencies are safe to group since they weren't modified in the PR
|
||||
all_isolated = set(ISOLATED_COMPONENTS.keys())
|
||||
if additional_isolated:
|
||||
all_isolated.update(additional_isolated)
|
||||
|
||||
# Group by (platform, bus_signature)
|
||||
for component, platforms in component_buses.items():
|
||||
if component not in all_tests:
|
||||
@@ -404,7 +417,7 @@ def run_grouped_component_tests(
|
||||
|
||||
# Skip components that must be tested in isolation
|
||||
# These are shown separately and should not be in non_groupable_reasons
|
||||
if component in ISOLATED_COMPONENTS:
|
||||
if component in all_isolated:
|
||||
continue
|
||||
|
||||
# Skip base bus components (these test the bus platforms themselves)
|
||||
@@ -453,15 +466,28 @@ def run_grouped_component_tests(
|
||||
print("\nGrouping Plan:")
|
||||
print("-" * 80)
|
||||
|
||||
# Show isolated components (must test individually due to known issues)
|
||||
isolated_in_tests = [c for c in ISOLATED_COMPONENTS if c in all_tests]
|
||||
# Show isolated components (must test individually due to known issues or direct changes)
|
||||
isolated_in_tests = [c for c in all_isolated if c in all_tests]
|
||||
if isolated_in_tests:
|
||||
print(
|
||||
f"\n⚠ {len(isolated_in_tests)} components must be tested in isolation (known build issues):"
|
||||
)
|
||||
for comp in sorted(isolated_in_tests):
|
||||
reason = ISOLATED_COMPONENTS[comp]
|
||||
print(f" - {comp}: {reason}")
|
||||
predefined_isolated = [c for c in isolated_in_tests if c in ISOLATED_COMPONENTS]
|
||||
additional_in_tests = [
|
||||
c for c in isolated_in_tests if c in (additional_isolated or set())
|
||||
]
|
||||
|
||||
if predefined_isolated:
|
||||
print(
|
||||
f"\n⚠ {len(predefined_isolated)} components must be tested in isolation (known build issues):"
|
||||
)
|
||||
for comp in sorted(predefined_isolated):
|
||||
reason = ISOLATED_COMPONENTS[comp]
|
||||
print(f" - {comp}: {reason}")
|
||||
|
||||
if additional_in_tests:
|
||||
print(
|
||||
f"\n✓ {len(additional_in_tests)} components tested in isolation (directly changed in PR):"
|
||||
)
|
||||
for comp in sorted(additional_in_tests):
|
||||
print(f" - {comp}")
|
||||
|
||||
# Show base bus components (test the bus platform implementations)
|
||||
base_bus_in_tests = [c for c in BASE_BUS_COMPONENTS if c in all_tests]
|
||||
@@ -733,6 +759,7 @@ def test_components(
|
||||
esphome_command: str,
|
||||
continue_on_fail: bool,
|
||||
enable_grouping: bool = True,
|
||||
isolated_components: set[str] | None = None,
|
||||
) -> int:
|
||||
"""Test components with optional intelligent grouping.
|
||||
|
||||
@@ -742,6 +769,10 @@ def test_components(
|
||||
esphome_command: ESPHome command (config/compile)
|
||||
continue_on_fail: Whether to continue on failure
|
||||
enable_grouping: Whether to enable component grouping
|
||||
isolated_components: Set of component names to test in isolation (not grouped).
|
||||
These are tested WITHOUT --testing-mode to enable full validation
|
||||
(pin conflicts, etc). This is used in CI for directly changed components
|
||||
to catch issues that would be missed with --testing-mode.
|
||||
|
||||
Returns:
|
||||
Exit code (0 for success, 1 for failure)
|
||||
@@ -788,6 +819,7 @@ def test_components(
|
||||
build_dir=build_dir,
|
||||
esphome_command=esphome_command,
|
||||
continue_on_fail=continue_on_fail,
|
||||
additional_isolated=isolated_components,
|
||||
)
|
||||
|
||||
# Then run individual tests for components not in groups
|
||||
@@ -912,18 +944,30 @@ def main() -> int:
|
||||
action="store_true",
|
||||
help="Disable component grouping (test each component individually)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--isolate",
|
||||
help="Comma-separated list of components to test in isolation (not grouped with others). "
|
||||
"These are tested WITHOUT --testing-mode to enable full validation. "
|
||||
"Used in CI for directly changed components to catch pin conflicts and other issues.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Parse component patterns
|
||||
component_patterns = [p.strip() for p in args.components.split(",")]
|
||||
|
||||
# Parse isolated components
|
||||
isolated_components = None
|
||||
if args.isolate:
|
||||
isolated_components = {c.strip() for c in args.isolate.split(",") if c.strip()}
|
||||
|
||||
return test_components(
|
||||
component_patterns=component_patterns,
|
||||
platform_filter=args.target,
|
||||
esphome_command=args.esphome_command,
|
||||
continue_on_fail=args.continue_on_fail,
|
||||
enable_grouping=not args.no_grouping,
|
||||
isolated_components=isolated_components,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ def run_schema_validation(config: ConfigType) -> None:
|
||||
{
|
||||
"id": "display_id",
|
||||
"model": "custom",
|
||||
"dimensions": {"width": 320, "height": 240},
|
||||
"dimensions": {"width": 260, "height": 260},
|
||||
"draw_rounding": 13,
|
||||
"init_sequence": [[0xA0, 0x01]],
|
||||
},
|
||||
@@ -336,7 +336,7 @@ def test_native_generation(
|
||||
|
||||
main_cpp = generate_main(component_fixture_path("native.yaml"))
|
||||
assert (
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1>()"
|
||||
"mipi_spi::MipiSpiBuffer<uint16_t, mipi_spi::PIXEL_MODE_16, true, mipi_spi::PIXEL_MODE_16, mipi_spi::BUS_TYPE_QUAD, 360, 360, 0, 1, display::DISPLAY_ROTATION_0_DEGREES, 1, 1>()"
|
||||
in main_cpp
|
||||
)
|
||||
assert "set_init_sequence({240, 1, 8, 242" in main_cpp
|
||||
|
||||
19
tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml
Normal file
19
tests/components/bluetooth_proxy/test.esp32-p4-idf.yaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<<: !include common.yaml
|
||||
|
||||
esp32_ble_tracker:
|
||||
max_connections: 9
|
||||
|
||||
bluetooth_proxy:
|
||||
active: true
|
||||
connection_slots: 9
|
||||
|
||||
esp32_hosted:
|
||||
active_high: true
|
||||
variant: ESP32C6
|
||||
reset_pin: GPIO54
|
||||
cmd_pin: GPIO19
|
||||
clk_pin: GPIO18
|
||||
d0_pin: GPIO14
|
||||
d1_pin: GPIO15
|
||||
d2_pin: GPIO16
|
||||
d3_pin: GPIO17
|
||||
@@ -7,8 +7,8 @@ display:
|
||||
id: ili9xxx_display
|
||||
model: GC9A01A
|
||||
invert_colors: True
|
||||
cs_pin: 10
|
||||
dc_pin: 6
|
||||
cs_pin: 11
|
||||
dc_pin: 7
|
||||
pages:
|
||||
- id: page1
|
||||
lambda: |-
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user