mirror of
https://github.com/esphome/esphome.git
synced 2025-11-18 15:55:46 +00:00
Merge branch 'integration' of https://github.com/esphome/esphome into integration
This commit is contained in:
15
.github/workflows/auto-label-pr.yml
vendored
15
.github/workflows/auto-label-pr.yml
vendored
@@ -416,7 +416,7 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Generate review messages
|
// Generate review messages
|
||||||
function generateReviewMessages(finalLabels) {
|
function generateReviewMessages(finalLabels, originalLabelCount) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
const prAuthor = context.payload.pull_request.user.login;
|
||||||
|
|
||||||
@@ -430,15 +430,15 @@ jobs:
|
|||||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||||
|
|
||||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
||||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||||
|
|
||||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||||
|
|
||||||
if (tooManyLabels && tooManyChanges) {
|
if (tooManyLabels && tooManyChanges) {
|
||||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
|
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||||
} else if (tooManyLabels) {
|
} else if (tooManyLabels) {
|
||||||
message += `This PR affects ${finalLabels.length} different components/areas.`;
|
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||||
} else {
|
} else {
|
||||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||||
}
|
}
|
||||||
@@ -466,8 +466,8 @@ jobs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle reviews
|
// Handle reviews
|
||||||
async function handleReviews(finalLabels) {
|
async function handleReviews(finalLabels, originalLabelCount) {
|
||||||
const reviewMessages = generateReviewMessages(finalLabels);
|
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
|
||||||
const hasReviewableLabels = finalLabels.some(label =>
|
const hasReviewableLabels = finalLabels.some(label =>
|
||||||
['too-big', 'needs-codeowners'].includes(label)
|
['too-big', 'needs-codeowners'].includes(label)
|
||||||
);
|
);
|
||||||
@@ -627,6 +627,7 @@ jobs:
|
|||||||
|
|
||||||
// Handle too many labels (only for non-mega PRs)
|
// Handle too many labels (only for non-mega PRs)
|
||||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||||
|
const originalLabelCount = finalLabels.length;
|
||||||
|
|
||||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||||
finalLabels = ['too-big'];
|
finalLabels = ['too-big'];
|
||||||
@@ -635,7 +636,7 @@ jobs:
|
|||||||
console.log('Computed labels:', finalLabels.join(', '));
|
console.log('Computed labels:', finalLabels.join(', '));
|
||||||
|
|
||||||
// Handle reviews
|
// Handle reviews
|
||||||
await handleReviews(finalLabels);
|
await handleReviews(finalLabels, originalLabelCount);
|
||||||
|
|
||||||
// Apply labels
|
// Apply labels
|
||||||
if (finalLabels.length > 0) {
|
if (finalLabels.length > 0) {
|
||||||
|
|||||||
2
.github/workflows/ci-api-proto.yml
vendored
2
.github/workflows/ci-api-proto.yml
vendored
@@ -62,7 +62,7 @@ jobs:
|
|||||||
run: git diff
|
run: git diff
|
||||||
- if: failure()
|
- if: failure()
|
||||||
name: Archive artifacts
|
name: Archive artifacts
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: generated-proto-files
|
name: generated-proto-files
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
62
.github/workflows/ci.yml
vendored
62
.github/workflows/ci.yml
vendored
@@ -114,7 +114,7 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
python-version:
|
python-version:
|
||||||
- "3.11"
|
- "3.11"
|
||||||
- "3.14"
|
- "3.13"
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- macOS-latest
|
- macOS-latest
|
||||||
@@ -123,9 +123,9 @@ jobs:
|
|||||||
# Minimize CI resource usage
|
# Minimize CI resource usage
|
||||||
# by only running the Python version
|
# by only running the Python version
|
||||||
# version used for docker images on Windows and macOS
|
# version used for docker images on Windows and macOS
|
||||||
- python-version: "3.14"
|
- python-version: "3.13"
|
||||||
os: windows-latest
|
os: windows-latest
|
||||||
- python-version: "3.14"
|
- python-version: "3.13"
|
||||||
os: macOS-latest
|
os: macOS-latest
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
needs:
|
needs:
|
||||||
@@ -180,6 +180,7 @@ jobs:
|
|||||||
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
memory_impact: ${{ steps.determine.outputs.memory-impact }}
|
||||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||||
|
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code from GitHub
|
- name: Check out code from GitHub
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
@@ -214,6 +215,7 @@ jobs:
|
|||||||
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
|
||||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||||
|
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
integration-tests:
|
integration-tests:
|
||||||
name: Run integration tests
|
name: Run integration tests
|
||||||
@@ -458,7 +460,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: 1
|
max-parallel: 2
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- id: clang-tidy
|
- id: clang-tidy
|
||||||
@@ -536,59 +538,18 @@ jobs:
|
|||||||
run: script/ci-suggest-changes
|
run: script/ci-suggest-changes
|
||||||
if: always()
|
if: always()
|
||||||
|
|
||||||
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) > 0
|
|
||||||
outputs:
|
|
||||||
matrix: ${{ steps.split.outputs.components }}
|
|
||||||
steps:
|
|
||||||
- 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: Split components intelligently based on bus configurations
|
|
||||||
id: split
|
|
||||||
run: |
|
|
||||||
. venv/bin/activate
|
|
||||||
|
|
||||||
# Use intelligent splitter that groups components with same bus configs
|
|
||||||
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
|
|
||||||
|
|
||||||
# Only isolate directly changed components when targeting dev branch
|
|
||||||
# For beta/release branches, group everything for faster CI
|
|
||||||
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
|
||||||
directly_changed='[]'
|
|
||||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
|
||||||
else
|
|
||||||
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
|
|
||||||
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Splitting components intelligently..."
|
|
||||||
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
|
|
||||||
|
|
||||||
echo "$output" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
test-build-components-split:
|
test-build-components-split:
|
||||||
name: Test components batch (${{ matrix.components }})
|
name: Test components batch (${{ matrix.components }})
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
- common
|
- common
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components-splitter
|
|
||||||
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
|
||||||
matrix:
|
matrix:
|
||||||
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
|
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||||
steps:
|
steps:
|
||||||
- name: Show disk space
|
- name: Show disk space
|
||||||
run: |
|
run: |
|
||||||
@@ -849,7 +810,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Upload memory analysis JSON
|
- name: Upload memory analysis JSON
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-target
|
name: memory-analysis-target
|
||||||
path: memory-analysis-target.json
|
path: memory-analysis-target.json
|
||||||
@@ -913,7 +874,7 @@ jobs:
|
|||||||
--platform "$platform"
|
--platform "$platform"
|
||||||
|
|
||||||
- name: Upload memory analysis JSON
|
- name: Upload memory analysis JSON
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-pr
|
name: memory-analysis-pr
|
||||||
path: memory-analysis-pr.json
|
path: memory-analysis-pr.json
|
||||||
@@ -943,13 +904,13 @@ jobs:
|
|||||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||||
- name: Download target analysis JSON
|
- name: Download target analysis JSON
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-target
|
name: memory-analysis-target
|
||||||
path: ./memory-analysis
|
path: ./memory-analysis
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
- name: Download PR analysis JSON
|
- name: Download PR analysis JSON
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: memory-analysis-pr
|
name: memory-analysis-pr
|
||||||
path: ./memory-analysis
|
path: ./memory-analysis
|
||||||
@@ -980,7 +941,6 @@ jobs:
|
|||||||
- clang-tidy-nosplit
|
- clang-tidy-nosplit
|
||||||
- clang-tidy-split
|
- clang-tidy-split
|
||||||
- determine-jobs
|
- determine-jobs
|
||||||
- test-build-components-splitter
|
|
||||||
- test-build-components-split
|
- test-build-components-split
|
||||||
- pre-commit-ci-lite
|
- pre-commit-ci-lite
|
||||||
- memory-impact-target-branch
|
- memory-impact-target-branch
|
||||||
|
|||||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
build-mode: ${{ matrix.build-mode }}
|
build-mode: ${{ matrix.build-mode }}
|
||||||
@@ -86,6 +86,6 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
|
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|||||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -138,7 +138,7 @@ jobs:
|
|||||||
# version: ${{ needs.init.outputs.tag }}
|
# version: ${{ needs.init.outputs.tag }}
|
||||||
|
|
||||||
- name: Upload digests
|
- name: Upload digests
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
||||||
with:
|
with:
|
||||||
name: digests-${{ matrix.platform.arch }}
|
name: digests-${{ matrix.platform.arch }}
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
@@ -171,7 +171,7 @@ jobs:
|
|||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||||
|
|
||||||
- name: Download digests
|
- name: Download digests
|
||||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
|
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||||
with:
|
with:
|
||||||
pattern: digests-*
|
pattern: digests-*
|
||||||
path: /tmp/digests
|
path: /tmp/digests
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ ci:
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||||
# Ruff version.
|
# Ruff version.
|
||||||
rev: v0.14.1
|
rev: v0.14.3
|
||||||
hooks:
|
hooks:
|
||||||
# Run the linter.
|
# Run the linter.
|
||||||
- id: ruff
|
- id: ruff
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
|||||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||||
esphome/components/hbridge/light/* @DotNetDann
|
esphome/components/hbridge/light/* @DotNetDann
|
||||||
esphome/components/hbridge/switch/* @dwmw2
|
esphome/components/hbridge/switch/* @dwmw2
|
||||||
|
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||||
esphome/components/he60r/* @clydebarrow
|
esphome/components/he60r/* @clydebarrow
|
||||||
esphome/components/heatpumpir/* @rob-deutsch
|
esphome/components/heatpumpir/* @rob-deutsch
|
||||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
# What does this implement/fix?
|
|
||||||
|
|
||||||
Removes redundant ESP32 Arduino test files for the `opentherm` component and cleans up redundant preprocessor conditionals. The ESP-IDF tests provide complete coverage since the opentherm component has no framework-specific implementation differences for ESP32.
|
|
||||||
|
|
||||||
Also fixes incorrect preprocessor conditionals - changes `#if defined(ESP32) || defined(USE_ESP_IDF)` to `#ifdef USE_ESP32`. The macro `ESP32` is only defined for the original ESP32 variant, while `USE_ESP32` covers all ESP32 variants (C3, S2, S3, etc.). The `|| defined(USE_ESP_IDF)` was unnecessary since ESP-IDF can only run on ESP32 platforms.
|
|
||||||
|
|
||||||
## Background
|
|
||||||
|
|
||||||
As part of the ongoing effort to reduce Arduino-specific test redundancy (esphome/backlog#66), this PR removes ESP32 Arduino tests that duplicate IDF test coverage.
|
|
||||||
|
|
||||||
**Analysis of opentherm component:**
|
|
||||||
- Previously used `#if defined(ESP32) || defined(USE_ESP_IDF)` to check for ESP32 **platform**
|
|
||||||
- This was incorrect: `ESP32` is only defined for the original ESP32 variant, not C3/S2/S3
|
|
||||||
- Changed to `#ifdef USE_ESP32` which covers all ESP32 variants
|
|
||||||
- The `|| defined(USE_ESP_IDF)` part was unnecessary since ESP-IDF can only run on ESP32 platforms
|
|
||||||
- ESP32 timer APIs (`timer_init`, `timer_set_counter_value`, `timer_isr_callback_add`) are ESP-IDF APIs
|
|
||||||
- These timer APIs work identically in both Arduino and ESP-IDF frameworks since Arduino is now built on ESP-IDF
|
|
||||||
- Only ESP8266 has framework-specific code (using Arduino's `timer1_*` functions)
|
|
||||||
- ESP32 implementation is identical across frameworks
|
|
||||||
|
|
||||||
## Changes
|
|
||||||
|
|
||||||
### Code Cleanup
|
|
||||||
|
|
||||||
**OpenTherm component:**
|
|
||||||
- Fixed incorrect `#if defined(ESP32) || defined(USE_ESP_IDF)` to `#ifdef USE_ESP32` in:
|
|
||||||
- `esphome/components/opentherm/opentherm.h` (3 locations)
|
|
||||||
- `esphome/components/opentherm/opentherm.cpp` (4 locations)
|
|
||||||
- `ESP32` is only defined for the original ESP32 variant, not C3/S2/S3
|
|
||||||
- `USE_ESP32` correctly covers all ESP32 variants
|
|
||||||
- The `|| defined(USE_ESP_IDF)` part was unnecessary since ESP-IDF can only be defined on ESP32 platforms
|
|
||||||
|
|
||||||
### Test Files Removed
|
|
||||||
|
|
||||||
- `tests/components/opentherm/test.esp32-ard.yaml`
|
|
||||||
- `tests/components/opentherm/test.esp32-c3-ard.yaml`
|
|
||||||
|
|
||||||
### Test Coverage Maintained
|
|
||||||
|
|
||||||
ESP-IDF test files remain and cover both frameworks:
|
|
||||||
- `tests/components/opentherm/test.esp32-idf.yaml`
|
|
||||||
- `tests/components/opentherm/test.esp32-c3-idf.yaml`
|
|
||||||
|
|
||||||
### Platform-Specific Tests Retained
|
|
||||||
|
|
||||||
Arduino tests remain for ESP8266 (uses Arduino-specific `timer1_*` functions):
|
|
||||||
- `tests/components/opentherm/test.esp8266-ard.yaml`
|
|
||||||
|
|
||||||
## Benefits
|
|
||||||
|
|
||||||
- **Reduces CI test time** - 2 fewer redundant test configurations
|
|
||||||
- **Simplifies code** - Removes redundant preprocessor conditionals
|
|
||||||
- **Maintains coverage** - IDF tests cover both frameworks for ESP32
|
|
||||||
|
|
||||||
## Types of changes
|
|
||||||
|
|
||||||
- [x] Code quality improvements to existing code or addition of tests
|
|
||||||
|
|
||||||
**Related issue or feature (if applicable):**
|
|
||||||
|
|
||||||
- Part of esphome/backlog#66 - Remove redundant ESP32 Arduino tests
|
|
||||||
|
|
||||||
**Pull request in [esphome-docs](https://github.com/esphome/esphome-docs) with documentation (if applicable):**
|
|
||||||
|
|
||||||
N/A - No user-facing changes
|
|
||||||
|
|
||||||
## Test Environment
|
|
||||||
|
|
||||||
- [x] ESP32
|
|
||||||
- [x] ESP32 IDF
|
|
||||||
- [ ] ESP8266
|
|
||||||
- [ ] RP2040
|
|
||||||
- [ ] BK72xx
|
|
||||||
- [ ] RTL87xx
|
|
||||||
- [ ] nRF52840
|
|
||||||
|
|
||||||
## Example entry for `config.yaml`:
|
|
||||||
|
|
||||||
N/A - No configuration changes
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
- [x] The code change is tested and works locally.
|
|
||||||
- [ ] Tests have been added to verify that the new code works (under `tests/` folder).
|
|
||||||
|
|
||||||
If user exposed functionality or configuration variables are added/changed:
|
|
||||||
- [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs).
|
|
||||||
@@ -207,14 +207,14 @@ def choose_upload_log_host(
|
|||||||
if has_mqtt_logging():
|
if has_mqtt_logging():
|
||||||
resolved.append("MQTT")
|
resolved.append("MQTT")
|
||||||
|
|
||||||
if has_api() and has_non_ip_address():
|
if has_api() and has_non_ip_address() and has_resolvable_address():
|
||||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||||
|
|
||||||
elif purpose == Purpose.UPLOADING:
|
elif purpose == Purpose.UPLOADING:
|
||||||
if has_ota() and has_mqtt_ip_lookup():
|
if has_ota() and has_mqtt_ip_lookup():
|
||||||
resolved.append("MQTTIP")
|
resolved.append("MQTTIP")
|
||||||
|
|
||||||
if has_ota() and has_non_ip_address():
|
if has_ota() and has_non_ip_address() and has_resolvable_address():
|
||||||
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
resolved.extend(_resolve_with_cache(CORE.address, purpose))
|
||||||
else:
|
else:
|
||||||
resolved.append(device)
|
resolved.append(device)
|
||||||
@@ -318,7 +318,17 @@ def has_resolvable_address() -> bool:
|
|||||||
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
|
||||||
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
|
||||||
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
|
||||||
return CORE.address is not None
|
if CORE.address is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if has_ip_address():
|
||||||
|
return True
|
||||||
|
|
||||||
|
if has_mdns():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# .local mDNS hostnames are only resolvable if mDNS is enabled
|
||||||
|
return not CORE.address.endswith(".local")
|
||||||
|
|
||||||
|
|
||||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
|
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,12 @@ from esphome.const import (
|
|||||||
CONF_UPDATE_INTERVAL,
|
CONF_UPDATE_INTERVAL,
|
||||||
)
|
)
|
||||||
from esphome.core import ID
|
from esphome.core import ID
|
||||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
from esphome.cpp_generator import (
|
||||||
|
LambdaExpression,
|
||||||
|
MockObj,
|
||||||
|
MockObjClass,
|
||||||
|
TemplateArgsType,
|
||||||
|
)
|
||||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
from esphome.util import Registry
|
from esphome.util import Registry
|
||||||
@@ -87,6 +92,7 @@ def validate_potentially_or_condition(value):
|
|||||||
|
|
||||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
|
||||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
|
||||||
|
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
|
||||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
IfAction = cg.esphome_ns.class_("IfAction", Action)
|
||||||
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
|
||||||
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
|
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
|
||||||
@@ -97,9 +103,40 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
|
|||||||
Automation = cg.esphome_ns.class_("Automation")
|
Automation = cg.esphome_ns.class_("Automation")
|
||||||
|
|
||||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
||||||
|
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
|
||||||
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
|
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
|
||||||
|
|
||||||
|
|
||||||
|
def new_lambda_pvariable(
|
||||||
|
id_obj: ID,
|
||||||
|
lambda_expr: LambdaExpression,
|
||||||
|
stateless_class: MockObjClass,
|
||||||
|
template_arg: cg.TemplateArguments | None = None,
|
||||||
|
) -> MockObj:
|
||||||
|
"""Create Pvariable for lambda, using stateless class if applicable.
|
||||||
|
|
||||||
|
Combines ID selection and Pvariable creation in one call. For stateless
|
||||||
|
lambdas (empty capture), uses function pointer instead of std::function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
id_obj: The ID object (action_id, condition_id, or filter_id)
|
||||||
|
lambda_expr: The lambda expression object
|
||||||
|
stateless_class: The stateless class to use for stateless lambdas
|
||||||
|
template_arg: Optional template arguments (for actions/conditions)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The created Pvariable
|
||||||
|
"""
|
||||||
|
# For stateless lambdas, use function pointer instead of std::function
|
||||||
|
if lambda_expr.capture == "":
|
||||||
|
id_obj = id_obj.copy()
|
||||||
|
id_obj.type = stateless_class
|
||||||
|
|
||||||
|
if template_arg is not None:
|
||||||
|
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
|
||||||
|
return cg.new_Pvariable(id_obj, lambda_expr)
|
||||||
|
|
||||||
|
|
||||||
def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||||
if extra_schema is None:
|
if extra_schema is None:
|
||||||
extra_schema = {}
|
extra_schema = {}
|
||||||
@@ -145,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
|||||||
value = cv.Schema([extra_validators])(value)
|
value = cv.Schema([extra_validators])(value)
|
||||||
if single:
|
if single:
|
||||||
if len(value) != 1:
|
if len(value) != 1:
|
||||||
raise cv.Invalid("Cannot have more than 1 automation for templates")
|
raise cv.Invalid("This trigger allows only a single automation")
|
||||||
return value[0]
|
return value[0]
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
|
|||||||
args: TemplateArgsType,
|
args: TemplateArgsType,
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
lambda_ = await cg.process_lambda(config, args, return_type=bool)
|
||||||
return cg.new_Pvariable(condition_id, template_arg, lambda_)
|
return new_lambda_pvariable(
|
||||||
|
condition_id, lambda_, StatelessLambdaCondition, template_arg
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@register_condition(
|
@register_condition(
|
||||||
@@ -406,7 +445,7 @@ async def lambda_action_to_code(
|
|||||||
args: TemplateArgsType,
|
args: TemplateArgsType,
|
||||||
) -> MockObj:
|
) -> MockObj:
|
||||||
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
|
||||||
return cg.new_Pvariable(action_id, template_arg, lambda_)
|
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||||
|
|
||||||
|
|
||||||
@register_action(
|
@register_action(
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ from esphome.cpp_types import ( # noqa: F401
|
|||||||
EntityBase,
|
EntityBase,
|
||||||
EntityCategory,
|
EntityCategory,
|
||||||
ESPTime,
|
ESPTime,
|
||||||
|
FixedVector,
|
||||||
GPIOPin,
|
GPIOPin,
|
||||||
InternalGPIOPin,
|
InternalGPIOPin,
|
||||||
JsonObject,
|
JsonObject,
|
||||||
|
|||||||
@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
|
|||||||
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
|
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
|
|
||||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
|
|
||||||
cv.deprecated_schema_constant("alarm_control_panel")
|
|
||||||
)
|
|
||||||
|
|
||||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
|
||||||
{
|
{
|
||||||
cv.GenerateID(): cv.use_id(AlarmControlPanel),
|
cv.GenerateID(): cv.use_id(AlarmControlPanel),
|
||||||
|
|||||||
@@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
|
|||||||
"int": cg.int32,
|
"int": cg.int32,
|
||||||
"float": float,
|
"float": float,
|
||||||
"string": cg.std_string,
|
"string": cg.std_string,
|
||||||
"bool[]": cg.std_vector.template(bool),
|
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
|
||||||
"int[]": cg.std_vector.template(cg.int32),
|
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||||
"float[]": cg.std_vector.template(float),
|
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
|
||||||
"string[]": cg.std_vector.template(cg.std_string),
|
"string[]": cg.FixedVector.template(cg.std_string)
|
||||||
|
.operator("const")
|
||||||
|
.operator("ref"),
|
||||||
}
|
}
|
||||||
CONF_ENCRYPTION = "encryption"
|
CONF_ENCRYPTION = "encryption"
|
||||||
CONF_BATCH_DELAY = "batch_delay"
|
CONF_BATCH_DELAY = "batch_delay"
|
||||||
@@ -258,6 +260,10 @@ async def to_code(config):
|
|||||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||||
cg.add_define("USE_API_SERVICES")
|
cg.add_define("USE_API_SERVICES")
|
||||||
|
|
||||||
|
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
|
||||||
|
if config[CONF_CUSTOM_SERVICES]:
|
||||||
|
cg.add_define("USE_API_CUSTOM_SERVICES")
|
||||||
|
|
||||||
if config[CONF_HOMEASSISTANT_SERVICES]:
|
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||||
|
|
||||||
@@ -265,6 +271,8 @@ async def to_code(config):
|
|||||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||||
|
|
||||||
if actions := config.get(CONF_ACTIONS, []):
|
if actions := config.get(CONF_ACTIONS, []):
|
||||||
|
# Collect all triggers first, then register all at once with initializer_list
|
||||||
|
triggers: list[cg.Pvariable] = []
|
||||||
for conf in actions:
|
for conf in actions:
|
||||||
template_args = []
|
template_args = []
|
||||||
func_args = []
|
func_args = []
|
||||||
@@ -278,8 +286,10 @@ async def to_code(config):
|
|||||||
trigger = cg.new_Pvariable(
|
trigger = cg.new_Pvariable(
|
||||||
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
|
||||||
)
|
)
|
||||||
cg.add(var.register_user_service(trigger))
|
triggers.append(trigger)
|
||||||
await automation.build_automation(trigger, func_args, conf)
|
await automation.build_automation(trigger, func_args, conf)
|
||||||
|
# Register all services at once - single allocation, no reallocations
|
||||||
|
cg.add(var.initialize_user_services(triggers))
|
||||||
|
|
||||||
if CONF_ON_CLIENT_CONNECTED in config:
|
if CONF_ON_CLIENT_CONNECTED in config:
|
||||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
|
|||||||
bool disabled_by_default = 9;
|
bool disabled_by_default = 9;
|
||||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||||
EntityCategory entity_category = 11;
|
EntityCategory entity_category = 11;
|
||||||
repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"];
|
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||||
}
|
}
|
||||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||||
@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
|
|||||||
|
|
||||||
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
||||||
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
|
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
|
||||||
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
|
repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
|
||||||
float visual_min_temperature = 8;
|
float visual_min_temperature = 8;
|
||||||
float visual_max_temperature = 9;
|
float visual_max_temperature = 9;
|
||||||
float visual_target_temperature_step = 10;
|
float visual_target_temperature_step = 10;
|
||||||
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
|
|||||||
// Deprecated in API version 1.5
|
// Deprecated in API version 1.5
|
||||||
bool legacy_supports_away = 11 [deprecated=true];
|
bool legacy_supports_away = 11 [deprecated=true];
|
||||||
bool supports_action = 12; // Deprecated: use feature_flags
|
bool supports_action = 12; // Deprecated: use feature_flags
|
||||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
|
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
|
||||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
|
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
|
||||||
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
|
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||||
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
|
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
|
||||||
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
|
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||||
bool disabled_by_default = 18;
|
bool disabled_by_default = 18;
|
||||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||||
EntityCategory entity_category = 20;
|
EntityCategory entity_category = 20;
|
||||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
|
|||||||
reserved 4; // Deprecated: was string unique_id
|
reserved 4; // Deprecated: was string unique_id
|
||||||
|
|
||||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||||
repeated string options = 6 [(container_pointer) = "std::vector"];
|
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||||
bool disabled_by_default = 7;
|
bool disabled_by_default = 7;
|
||||||
EntityCategory entity_category = 8;
|
EntityCategory entity_category = 8;
|
||||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||||
|
|||||||
@@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
|||||||
}
|
}
|
||||||
if (traits.supports_direction())
|
if (traits.supports_direction())
|
||||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||||
if (traits.supports_preset_modes())
|
if (traits.supports_preset_modes() && fan->has_preset_mode())
|
||||||
msg.set_preset_mode(StringRef(fan->preset_mode));
|
msg.set_preset_mode(StringRef(fan->get_preset_mode()));
|
||||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||||
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
|||||||
msg.supports_speed = traits.supports_speed();
|
msg.supports_speed = traits.supports_speed();
|
||||||
msg.supports_direction = traits.supports_direction();
|
msg.supports_direction = traits.supports_direction();
|
||||||
msg.supported_speed_count = traits.supported_speed_count();
|
msg.supported_speed_count = traits.supported_speed_count();
|
||||||
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
msg.supported_preset_modes = &traits.supported_preset_modes();
|
||||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||||
@@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
|||||||
if (light->supports_effects()) {
|
if (light->supports_effects()) {
|
||||||
msg.effects.emplace_back("None");
|
msg.effects.emplace_back("None");
|
||||||
for (auto *effect : light->get_effects()) {
|
for (auto *effect : light->get_effects()) {
|
||||||
msg.effects.push_back(effect->get_name());
|
msg.effects.emplace_back(effect->get_name());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
@@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
|||||||
}
|
}
|
||||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
|
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
|
||||||
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
|
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
|
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
|
||||||
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
|
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes())
|
if (traits.get_supports_swing_modes())
|
||||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||||
@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
|||||||
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
|
||||||
// Current feature flags and other supported parameters
|
// Current feature flags and other supported parameters
|
||||||
msg.feature_flags = traits.get_feature_flags();
|
msg.feature_flags = traits.get_feature_flags();
|
||||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
msg.supported_modes = &traits.get_supported_modes();
|
||||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||||
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
||||||
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
|
||||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||||
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
msg.supported_fan_modes = &traits.get_supported_fan_modes();
|
||||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
|
||||||
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
msg.supported_presets = &traits.get_supported_presets();
|
||||||
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
|
msg.supported_custom_presets = &traits.get_supported_custom_presets();
|
||||||
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
|
msg.supported_swing_modes = &traits.get_supported_swing_modes();
|
||||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||||
is_single);
|
is_single);
|
||||||
}
|
}
|
||||||
@@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
|
|||||||
bool is_single) {
|
bool is_single) {
|
||||||
auto *select = static_cast<select::Select *>(entity);
|
auto *select = static_cast<select::Select *>(entity);
|
||||||
SelectStateResponse resp;
|
SelectStateResponse resp;
|
||||||
resp.set_state(StringRef(select->state));
|
resp.set_state(StringRef(select->current_option()));
|
||||||
resp.missing_state = !select->has_state();
|
resp.missing_state = !select->has_state();
|
||||||
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -434,8 +434,7 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
|||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
|
||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
|||||||
@@ -230,8 +230,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
|||||||
return APIError::OK;
|
return APIError::OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
|
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||||
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
|
|
||||||
|
|
||||||
this->reusable_iovs_.clear();
|
this->reusable_iovs_.clear();
|
||||||
this->reusable_iovs_.reserve(packets.size());
|
this->reusable_iovs_.reserve(packets.size());
|
||||||
|
|||||||
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
buffer.encode_string(10, this->icon_ref_);
|
buffer.encode_string(10, this->icon_ref_);
|
||||||
#endif
|
#endif
|
||||||
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
|
||||||
for (const auto &it : *this->supported_preset_modes) {
|
for (const char *it : *this->supported_preset_modes) {
|
||||||
buffer.encode_string(12, it, true);
|
buffer.encode_string(12, it, strlen(it), true);
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
buffer.encode_uint32(13, this->device_id);
|
buffer.encode_uint32(13, this->device_id);
|
||||||
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
|
|||||||
#endif
|
#endif
|
||||||
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
|
||||||
if (!this->supported_preset_modes->empty()) {
|
if (!this->supported_preset_modes->empty()) {
|
||||||
for (const auto &it : *this->supported_preset_modes) {
|
for (const char *it : *this->supported_preset_modes) {
|
||||||
size.add_length_force(1, it.size());
|
size.add_length_force(1, strlen(it));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#ifdef USE_DEVICES
|
#ifdef USE_DEVICES
|
||||||
@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
for (const auto &it : *this->supported_swing_modes) {
|
for (const auto &it : *this->supported_swing_modes) {
|
||||||
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
|
buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
|
||||||
}
|
}
|
||||||
for (const auto &it : *this->supported_custom_fan_modes) {
|
for (const char *it : *this->supported_custom_fan_modes) {
|
||||||
buffer.encode_string(15, it, true);
|
buffer.encode_string(15, it, strlen(it), true);
|
||||||
}
|
}
|
||||||
for (const auto &it : *this->supported_presets) {
|
for (const auto &it : *this->supported_presets) {
|
||||||
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
|
buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
|
||||||
}
|
}
|
||||||
for (const auto &it : *this->supported_custom_presets) {
|
for (const char *it : *this->supported_custom_presets) {
|
||||||
buffer.encode_string(17, it, true);
|
buffer.encode_string(17, it, strlen(it), true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(18, this->disabled_by_default);
|
buffer.encode_bool(18, this->disabled_by_default);
|
||||||
#ifdef USE_ENTITY_ICON
|
#ifdef USE_ENTITY_ICON
|
||||||
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_custom_fan_modes->empty()) {
|
if (!this->supported_custom_fan_modes->empty()) {
|
||||||
for (const auto &it : *this->supported_custom_fan_modes) {
|
for (const char *it : *this->supported_custom_fan_modes) {
|
||||||
size.add_length_force(1, it.size());
|
size.add_length_force(1, strlen(it));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_presets->empty()) {
|
if (!this->supported_presets->empty()) {
|
||||||
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!this->supported_custom_presets->empty()) {
|
if (!this->supported_custom_presets->empty()) {
|
||||||
for (const auto &it : *this->supported_custom_presets) {
|
for (const char *it : *this->supported_custom_presets) {
|
||||||
size.add_length_force(2, it.size());
|
size.add_length_force(2, strlen(it));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size.add_bool(2, this->disabled_by_default);
|
size.add_bool(2, this->disabled_by_default);
|
||||||
@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
|
|||||||
#ifdef USE_ENTITY_ICON
|
#ifdef USE_ENTITY_ICON
|
||||||
buffer.encode_string(5, this->icon_ref_);
|
buffer.encode_string(5, this->icon_ref_);
|
||||||
#endif
|
#endif
|
||||||
for (const auto &it : *this->options) {
|
for (const char *it : *this->options) {
|
||||||
buffer.encode_string(6, it, true);
|
buffer.encode_string(6, it, strlen(it), true);
|
||||||
}
|
}
|
||||||
buffer.encode_bool(7, this->disabled_by_default);
|
buffer.encode_bool(7, this->disabled_by_default);
|
||||||
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
|
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
|
||||||
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
|
|||||||
size.add_length(1, this->icon_ref_.size());
|
size.add_length(1, this->icon_ref_.size());
|
||||||
#endif
|
#endif
|
||||||
if (!this->options->empty()) {
|
if (!this->options->empty()) {
|
||||||
for (const auto &it : *this->options) {
|
for (const char *it : *this->options) {
|
||||||
size.add_length_force(1, it.size());
|
size.add_length_force(1, strlen(it));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
size.add_bool(1, this->disabled_by_default);
|
size.add_bool(1, this->disabled_by_default);
|
||||||
|
|||||||
@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
|
|||||||
bool supports_speed{false};
|
bool supports_speed{false};
|
||||||
bool supports_direction{false};
|
bool supports_direction{false};
|
||||||
int32_t supported_speed_count{0};
|
int32_t supported_speed_count{0};
|
||||||
const std::vector<std::string> *supported_preset_modes{};
|
const std::vector<const char *> *supported_preset_modes{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
|
|||||||
#endif
|
#endif
|
||||||
bool supports_current_temperature{false};
|
bool supports_current_temperature{false};
|
||||||
bool supports_two_point_target_temperature{false};
|
bool supports_two_point_target_temperature{false};
|
||||||
const std::set<climate::ClimateMode> *supported_modes{};
|
const climate::ClimateModeMask *supported_modes{};
|
||||||
float visual_min_temperature{0.0f};
|
float visual_min_temperature{0.0f};
|
||||||
float visual_max_temperature{0.0f};
|
float visual_max_temperature{0.0f};
|
||||||
float visual_target_temperature_step{0.0f};
|
float visual_target_temperature_step{0.0f};
|
||||||
bool supports_action{false};
|
bool supports_action{false};
|
||||||
const std::set<climate::ClimateFanMode> *supported_fan_modes{};
|
const climate::ClimateFanModeMask *supported_fan_modes{};
|
||||||
const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
|
const climate::ClimateSwingModeMask *supported_swing_modes{};
|
||||||
const std::set<std::string> *supported_custom_fan_modes{};
|
const std::vector<const char *> *supported_custom_fan_modes{};
|
||||||
const std::set<climate::ClimatePreset> *supported_presets{};
|
const climate::ClimatePresetMask *supported_presets{};
|
||||||
const std::set<std::string> *supported_custom_presets{};
|
const std::vector<const char *> *supported_custom_presets{};
|
||||||
float visual_current_temperature_step{0.0f};
|
float visual_current_temperature_step{0.0f};
|
||||||
bool supports_current_humidity{false};
|
bool supports_current_humidity{false};
|
||||||
bool supports_target_humidity{false};
|
bool supports_target_humidity{false};
|
||||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
|
|||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
const char *message_name() const override { return "list_entities_select_response"; }
|
const char *message_name() const override { return "list_entities_select_response"; }
|
||||||
#endif
|
#endif
|
||||||
const std::vector<std::string> *options{};
|
const FixedVector<const char *> *options{};
|
||||||
void encode(ProtoWriteBuffer buffer) const override;
|
void encode(ProtoWriteBuffer buffer) const override;
|
||||||
void calculate_size(ProtoSize &size) const override;
|
void calculate_size(ProtoSize &size) const override;
|
||||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
|
|||||||
out.append("\n");
|
out.append("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
|
||||||
|
append_field_prefix(out, field_name, indent);
|
||||||
|
out.append("'").append(value).append("'");
|
||||||
|
out.append("\n");
|
||||||
|
}
|
||||||
|
|
||||||
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
|
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
|
||||||
append_field_prefix(out, field_name, indent);
|
append_field_prefix(out, field_name, indent);
|
||||||
out.append(proto_enum_to_string<T>(value));
|
out.append(proto_enum_to_string<T>(value));
|
||||||
|
|||||||
@@ -125,8 +125,14 @@ class APIServer : public Component, public Controller {
|
|||||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
|
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
|
||||||
|
this->user_services_.assign(services);
|
||||||
|
}
|
||||||
|
#ifdef USE_API_CUSTOM_SERVICES
|
||||||
|
// Only compile push_back method when custom_services: true (external components)
|
||||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||||
#endif
|
#endif
|
||||||
|
#endif
|
||||||
#ifdef USE_HOMEASSISTANT_TIME
|
#ifdef USE_HOMEASSISTANT_TIME
|
||||||
void request_time();
|
void request_time();
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -53,8 +53,14 @@ class CustomAPIDevice {
|
|||||||
template<typename T, typename... Ts>
|
template<typename T, typename... Ts>
|
||||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||||
|
#ifdef USE_API_CUSTOM_SERVICES
|
||||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
|
#else
|
||||||
|
static_assert(
|
||||||
|
sizeof(T) == 0,
|
||||||
|
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
template<typename T, typename... Ts>
|
template<typename T, typename... Ts>
|
||||||
@@ -86,8 +92,14 @@ class CustomAPIDevice {
|
|||||||
*/
|
*/
|
||||||
#ifdef USE_API_SERVICES
|
#ifdef USE_API_SERVICES
|
||||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||||
|
#ifdef USE_API_CUSTOM_SERVICES
|
||||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||||
global_api_server->register_user_service(service);
|
global_api_server->register_user_service(service);
|
||||||
|
#else
|
||||||
|
static_assert(
|
||||||
|
sizeof(T) == 0,
|
||||||
|
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||||
|
|||||||
@@ -11,23 +11,58 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
|||||||
}
|
}
|
||||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||||
|
|
||||||
|
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
||||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||||
return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
|
std::vector<bool> result;
|
||||||
|
result.reserve(arg.bool_array.size());
|
||||||
|
result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
|
||||||
return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
|
std::vector<int32_t> result;
|
||||||
|
result.reserve(arg.int_array.size());
|
||||||
|
result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
|
||||||
return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
|
std::vector<float> result;
|
||||||
|
result.reserve(arg.float_array.size());
|
||||||
|
result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
|
||||||
return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
|
std::vector<std::string> result;
|
||||||
|
result.reserve(arg.string_array.size());
|
||||||
|
result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// New FixedVector const reference versions for YAML-generated services - zero-copy
|
||||||
|
template<>
|
||||||
|
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
|
||||||
|
return arg.bool_array;
|
||||||
|
}
|
||||||
|
template<>
|
||||||
|
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
|
||||||
|
return arg.int_array;
|
||||||
|
}
|
||||||
|
template<>
|
||||||
|
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
|
||||||
|
return arg.float_array;
|
||||||
|
}
|
||||||
|
template<>
|
||||||
|
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
|
||||||
|
const ExecuteServiceArgument &arg) {
|
||||||
|
return arg.string_array;
|
||||||
}
|
}
|
||||||
|
|
||||||
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
|
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||||
|
|
||||||
|
// Legacy std::vector versions for external components using custom_api_device.h
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
|
||||||
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
||||||
@@ -39,4 +74,18 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
|
|||||||
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New FixedVector const reference versions for YAML-generated services
|
||||||
|
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
|
||||||
|
return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
|
||||||
|
}
|
||||||
|
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
|
||||||
|
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
|
||||||
|
}
|
||||||
|
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
|
||||||
|
return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
|
||||||
|
}
|
||||||
|
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
|
||||||
|
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::api
|
} // namespace esphome::api
|
||||||
|
|||||||
@@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t {
|
|||||||
|
|
||||||
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
|
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
|
||||||
|
|
||||||
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
|
||||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
|
|
||||||
|
|
||||||
} // namespace bedjet
|
} // namespace bedjet
|
||||||
} // namespace esphome
|
} // namespace esphome
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
|
||||||
}
|
}
|
||||||
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
for (const auto &mode : traits.get_supported_custom_fan_modes()) {
|
||||||
ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
|
ESP_LOGCONFIG(TAG, " - %s (c)", mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGCONFIG(TAG, " Supported presets:");
|
ESP_LOGCONFIG(TAG, " Supported presets:");
|
||||||
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
|
|||||||
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
|
||||||
}
|
}
|
||||||
for (const auto &preset : traits.get_supported_custom_presets()) {
|
for (const auto &preset : traits.get_supported_custom_presets()) {
|
||||||
ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
|
ESP_LOGCONFIG(TAG, " - %s (c)", preset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
|
|||||||
this->target_temperature = NAN;
|
this->target_temperature = NAN;
|
||||||
this->current_temperature = NAN;
|
this->current_temperature = NAN;
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
if (button_result) {
|
if (button_result) {
|
||||||
this->mode = mode;
|
this->mode = mode;
|
||||||
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->mode = CLIMATE_MODE_HEAT;
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
this->preset = CLIMATE_PRESET_BOOST;
|
this->set_preset_(CLIMATE_PRESET_BOOST);
|
||||||
this->custom_preset.reset();
|
|
||||||
}
|
}
|
||||||
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
|
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
|
||||||
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
|
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
|
||||||
@@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
result = this->parent_->send_button(heat_button(this->heating_mode_));
|
result = this->parent_->send_button(heat_button(this->heating_mode_));
|
||||||
if (result) {
|
if (result) {
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
|
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
|
||||||
@@ -184,8 +183,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->custom_preset = preset;
|
this->set_custom_preset_(preset.c_str());
|
||||||
this->preset.reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +205,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
this->fan_mode = fan_mode;
|
this->set_fan_mode_(fan_mode);
|
||||||
this->custom_fan_mode.reset();
|
|
||||||
}
|
}
|
||||||
} else if (call.get_custom_fan_mode().has_value()) {
|
} else if (call.get_custom_fan_mode().has_value()) {
|
||||||
auto fan_mode = *call.get_custom_fan_mode();
|
auto fan_mode = *call.get_custom_fan_mode();
|
||||||
@@ -218,8 +215,7 @@ void BedJetClimate::control(const ClimateCall &call) {
|
|||||||
fan_index);
|
fan_index);
|
||||||
bool result = this->parent_->set_fan_index(fan_index);
|
bool result = this->parent_->set_fan_index(fan_index);
|
||||||
if (result) {
|
if (result) {
|
||||||
this->custom_fan_mode = fan_mode;
|
this->set_custom_fan_mode_(fan_mode.c_str());
|
||||||
this->fan_mode.reset();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +241,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
|
|
||||||
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
|
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
|
||||||
if (fan_mode_name != nullptr) {
|
if (fan_mode_name != nullptr) {
|
||||||
this->custom_fan_mode = *fan_mode_name;
|
this->set_custom_fan_mode_(fan_mode_name->c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
|
||||||
@@ -255,7 +251,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
this->mode = CLIMATE_MODE_OFF;
|
this->mode = CLIMATE_MODE_OFF;
|
||||||
this->action = CLIMATE_ACTION_IDLE;
|
this->action = CLIMATE_ACTION_IDLE;
|
||||||
this->fan_mode = CLIMATE_FAN_OFF;
|
this->fan_mode = CLIMATE_FAN_OFF;
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -266,7 +262,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||||
this->set_custom_preset_("LTD HT");
|
this->set_custom_preset_("LTD HT");
|
||||||
} else {
|
} else {
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -275,7 +271,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
this->action = CLIMATE_ACTION_HEATING;
|
this->action = CLIMATE_ACTION_HEATING;
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
} else {
|
} else {
|
||||||
this->set_custom_preset_("EXT HT");
|
this->set_custom_preset_("EXT HT");
|
||||||
}
|
}
|
||||||
@@ -284,20 +280,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
|
|||||||
case MODE_COOL:
|
case MODE_COOL:
|
||||||
this->mode = CLIMATE_MODE_FAN_ONLY;
|
this->mode = CLIMATE_MODE_FAN_ONLY;
|
||||||
this->action = CLIMATE_ACTION_COOLING;
|
this->action = CLIMATE_ACTION_COOLING;
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_DRY:
|
case MODE_DRY:
|
||||||
this->mode = CLIMATE_MODE_DRY;
|
this->mode = CLIMATE_MODE_DRY;
|
||||||
this->action = CLIMATE_ACTION_DRYING;
|
this->action = CLIMATE_ACTION_DRYING;
|
||||||
this->custom_preset.reset();
|
this->clear_custom_preset_();
|
||||||
this->preset.reset();
|
this->preset.reset();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MODE_TURBO:
|
case MODE_TURBO:
|
||||||
this->preset = CLIMATE_PRESET_BOOST;
|
this->set_preset_(CLIMATE_PRESET_BOOST);
|
||||||
this->custom_preset.reset();
|
|
||||||
this->mode = CLIMATE_MODE_HEAT;
|
this->mode = CLIMATE_MODE_HEAT;
|
||||||
this->action = CLIMATE_ACTION_HEATING;
|
this->action = CLIMATE_ACTION_HEATING;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -43,28 +43,20 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
|||||||
});
|
});
|
||||||
|
|
||||||
// It would be better if we had a slider for the fan modes.
|
// It would be better if we had a slider for the fan modes.
|
||||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
|
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||||
traits.set_supported_presets({
|
traits.set_supported_presets({
|
||||||
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
||||||
// climate::CLIMATE_PRESET_NONE,
|
// climate::CLIMATE_PRESET_NONE,
|
||||||
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
||||||
climate::CLIMATE_PRESET_BOOST,
|
climate::CLIMATE_PRESET_BOOST,
|
||||||
});
|
});
|
||||||
|
// String literals are stored in rodata and valid for program lifetime
|
||||||
traits.set_supported_custom_presets({
|
traits.set_supported_custom_presets({
|
||||||
// We could fetch biodata from bedjet and set these names that way.
|
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||||
// But then we have to invert the lookup in order to send the right preset.
|
|
||||||
// For now, we can leave them as M1-3 to match the remote buttons.
|
|
||||||
// EXT HT added to match remote button.
|
|
||||||
"EXT HT",
|
|
||||||
"M1",
|
"M1",
|
||||||
"M2",
|
"M2",
|
||||||
"M3",
|
"M3",
|
||||||
});
|
});
|
||||||
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
|
|
||||||
traits.add_supported_custom_preset("LTD HT");
|
|
||||||
} else {
|
|
||||||
traits.add_supported_custom_preset("EXT HT");
|
|
||||||
}
|
|
||||||
traits.set_visual_min_temperature(19.0);
|
traits.set_visual_min_temperature(19.0);
|
||||||
traits.set_visual_max_temperature(43.0);
|
traits.set_visual_max_temperature(43.0);
|
||||||
traits.set_visual_temperature_step(1.0);
|
traits.set_visual_temperature_step(1.0);
|
||||||
|
|||||||
@@ -155,6 +155,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
|
|||||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
|
||||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
|
||||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
|
||||||
|
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
|
||||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
|
||||||
|
|
||||||
_LOGGER = getLogger(__name__)
|
_LOGGER = getLogger(__name__)
|
||||||
@@ -299,7 +300,7 @@ async def lambda_filter_to_code(config, filter_id):
|
|||||||
lambda_ = await cg.process_lambda(
|
lambda_ = await cg.process_lambda(
|
||||||
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
config, [(bool, "x")], return_type=cg.optional.template(bool)
|
||||||
)
|
)
|
||||||
return cg.new_Pvariable(filter_id, lambda_)
|
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
|
||||||
|
|
||||||
|
|
||||||
@register_filter(
|
@register_filter(
|
||||||
@@ -547,11 +548,6 @@ def binary_sensor_schema(
|
|||||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
|
|
||||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_binary_sensor_core_(var, config):
|
async def setup_binary_sensor_core_(var, config):
|
||||||
await setup_entity(var, config, "binary_sensor")
|
await setup_entity(var, config, "binary_sensor")
|
||||||
|
|
||||||
|
|||||||
@@ -111,6 +111,21 @@ class LambdaFilter : public Filter {
|
|||||||
std::function<optional<bool>(bool)> f_;
|
std::function<optional<bool>(bool)> f_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Optimized lambda filter for stateless lambdas (no capture).
|
||||||
|
*
|
||||||
|
* Uses function pointer instead of std::function to reduce memory overhead.
|
||||||
|
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
|
||||||
|
*/
|
||||||
|
class StatelessLambdaFilter : public Filter {
|
||||||
|
public:
|
||||||
|
explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
|
||||||
|
|
||||||
|
optional<bool> new_value(bool value) override { return this->f_(value); }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
optional<bool> (*f_)(bool);
|
||||||
|
};
|
||||||
|
|
||||||
class SettleFilter : public Filter, public Component {
|
class SettleFilter : public Filter, public Component {
|
||||||
public:
|
public:
|
||||||
optional<bool> new_value(bool value) override;
|
optional<bool> new_value(bool value) override;
|
||||||
|
|||||||
@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
BLEClientWriteAction(BLEClient *ble_client) {
|
BLEClientWriteAction(BLEClient *ble_client) {
|
||||||
ble_client->register_ble_node(this);
|
ble_client->register_ble_node(this);
|
||||||
ble_client_ = ble_client;
|
ble_client_ = ble_client;
|
||||||
|
this->construct_simple_value_();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
~BLEClientWriteAction() { this->destroy_simple_value_(); }
|
||||||
|
|
||||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||||
@@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||||
|
|
||||||
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
|
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||||
this->value_template_ = std::move(func);
|
this->destroy_simple_value_();
|
||||||
has_simple_value_ = false;
|
this->value_.template_func = func;
|
||||||
|
this->has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_simple(const std::vector<uint8_t> &value) {
|
void set_value_simple(const std::vector<uint8_t> &value) {
|
||||||
this->value_simple_ = value;
|
if (!this->has_simple_value_) {
|
||||||
has_simple_value_ = true;
|
this->construct_simple_value_();
|
||||||
|
}
|
||||||
|
this->value_.simple = value;
|
||||||
|
this->has_simple_value_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void play(Ts... x) override {}
|
void play(Ts... x) override {}
|
||||||
@@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
void play_complex(Ts... x) override {
|
void play_complex(Ts... x) override {
|
||||||
this->num_running_++;
|
this->num_running_++;
|
||||||
this->var_ = std::make_tuple(x...);
|
this->var_ = std::make_tuple(x...);
|
||||||
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
|
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
|
||||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||||
if (!write(value))
|
if (!write(value))
|
||||||
this->play_next_(x...);
|
this->play_next_(x...);
|
||||||
@@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
|
||||||
|
|
||||||
|
void destroy_simple_value_() {
|
||||||
|
if (this->has_simple_value_) {
|
||||||
|
this->value_.simple.~vector();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
BLEClient *ble_client_;
|
BLEClient *ble_client_;
|
||||||
bool has_simple_value_ = true;
|
bool has_simple_value_ = true;
|
||||||
std::vector<uint8_t> value_simple_;
|
union Value {
|
||||||
std::function<std::vector<uint8_t>(Ts...)> value_template_{};
|
std::vector<uint8_t> simple;
|
||||||
|
std::vector<uint8_t> (*template_func)(Ts...);
|
||||||
|
Value() {} // trivial constructor
|
||||||
|
~Value() {} // trivial destructor - we manage lifetime via discriminator
|
||||||
|
} value_;
|
||||||
espbt::ESPBTUUID service_uuid_;
|
espbt::ESPBTUUID service_uuid_;
|
||||||
espbt::ESPBTUUID char_uuid_;
|
espbt::ESPBTUUID char_uuid_;
|
||||||
std::tuple<Ts...> var_{};
|
std::tuple<Ts...> var_{};
|
||||||
@@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
|||||||
void play(Ts... x) override {
|
void play(Ts... x) override {
|
||||||
uint32_t passkey;
|
uint32_t passkey;
|
||||||
if (has_simple_value_) {
|
if (has_simple_value_) {
|
||||||
passkey = this->value_simple_;
|
passkey = this->value_.simple;
|
||||||
} else {
|
} else {
|
||||||
passkey = this->value_template_(x...);
|
passkey = this->value_.template_func(x...);
|
||||||
}
|
}
|
||||||
if (passkey > 999999)
|
if (passkey > 999999)
|
||||||
return;
|
return;
|
||||||
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
|
|||||||
esp_ble_passkey_reply(remote_bda, true, passkey);
|
esp_ble_passkey_reply(remote_bda, true, passkey);
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_template(std::function<uint32_t(Ts...)> func) {
|
void set_value_template(uint32_t (*func)(Ts...)) {
|
||||||
this->value_template_ = std::move(func);
|
this->value_.template_func = func;
|
||||||
has_simple_value_ = false;
|
this->has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_simple(const uint32_t &value) {
|
void set_value_simple(const uint32_t &value) {
|
||||||
this->value_simple_ = value;
|
this->value_.simple = value;
|
||||||
has_simple_value_ = true;
|
this->has_simple_value_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BLEClient *parent_{nullptr};
|
BLEClient *parent_{nullptr};
|
||||||
bool has_simple_value_ = true;
|
bool has_simple_value_ = true;
|
||||||
uint32_t value_simple_{0};
|
union {
|
||||||
std::function<uint32_t(Ts...)> value_template_{};
|
uint32_t simple;
|
||||||
|
uint32_t (*template_func)(Ts...);
|
||||||
|
} value_{.simple = 0};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
|
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
|
||||||
@@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
|
|||||||
esp_bd_addr_t remote_bda;
|
esp_bd_addr_t remote_bda;
|
||||||
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
|
||||||
if (has_simple_value_) {
|
if (has_simple_value_) {
|
||||||
esp_ble_confirm_reply(remote_bda, this->value_simple_);
|
esp_ble_confirm_reply(remote_bda, this->value_.simple);
|
||||||
} else {
|
} else {
|
||||||
esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
|
esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_template(std::function<bool(Ts...)> func) {
|
void set_value_template(bool (*func)(Ts...)) {
|
||||||
this->value_template_ = std::move(func);
|
this->value_.template_func = func;
|
||||||
has_simple_value_ = false;
|
this->has_simple_value_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_value_simple(const bool &value) {
|
void set_value_simple(const bool &value) {
|
||||||
this->value_simple_ = value;
|
this->value_.simple = value;
|
||||||
has_simple_value_ = true;
|
this->has_simple_value_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
BLEClient *parent_{nullptr};
|
BLEClient *parent_{nullptr};
|
||||||
bool has_simple_value_ = true;
|
bool has_simple_value_ = true;
|
||||||
bool value_simple_{false};
|
union {
|
||||||
std::function<bool(Ts...)> value_template_{};
|
bool simple;
|
||||||
|
bool (*template_func)(Ts...);
|
||||||
|
} value_{.simple = false};
|
||||||
};
|
};
|
||||||
|
|
||||||
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
|
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
|
||||||
|
|||||||
@@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
|||||||
}
|
}
|
||||||
|
|
||||||
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
||||||
if (this->data_to_value_func_.has_value()) {
|
if (this->has_data_to_value_) {
|
||||||
std::vector<uint8_t> data(value, value + value_len);
|
std::vector<uint8_t> data(value, value + value_len);
|
||||||
return (*this->data_to_value_func_)(data);
|
return this->data_to_value_func_(data);
|
||||||
} else {
|
} else {
|
||||||
return value[0];
|
return value[0];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ namespace ble_client {
|
|||||||
|
|
||||||
namespace espbt = esphome::esp32_ble_tracker;
|
namespace espbt = esphome::esp32_ble_tracker;
|
||||||
|
|
||||||
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
|
|
||||||
|
|
||||||
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
|
||||||
public:
|
public:
|
||||||
void loop() override;
|
void loop() override;
|
||||||
@@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
|||||||
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||||
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
|
void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
|
||||||
|
this->data_to_value_func_ = lambda;
|
||||||
|
this->has_data_to_value_ = true;
|
||||||
|
}
|
||||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||||
uint16_t handle;
|
uint16_t handle;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
float parse_data_(uint8_t *value, uint16_t value_len);
|
float parse_data_(uint8_t *value, uint16_t value_len);
|
||||||
optional<data_to_value_t> data_to_value_func_{};
|
bool has_data_to_value_{false};
|
||||||
|
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
|
||||||
bool notify_;
|
bool notify_;
|
||||||
espbt::ESPBTUUID service_uuid_;
|
espbt::ESPBTUUID service_uuid_;
|
||||||
espbt::ESPBTUUID char_uuid_;
|
espbt::ESPBTUUID char_uuid_;
|
||||||
|
|||||||
@@ -84,11 +84,6 @@ def button_schema(
|
|||||||
return _BUTTON_SCHEMA.extend(schema)
|
return _BUTTON_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
BUTTON_SCHEMA = button_schema(Button)
|
|
||||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_button_core_(var, config):
|
async def setup_button_core_(var, config):
|
||||||
await setup_entity(var, config, "button")
|
await setup_entity(var, config, "button")
|
||||||
|
|
||||||
|
|||||||
@@ -270,11 +270,6 @@ def climate_schema(
|
|||||||
return _CLIMATE_SCHEMA.extend(schema)
|
return _CLIMATE_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
CLIMATE_SCHEMA = climate_schema(Climate)
|
|
||||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_climate_core_(var, config):
|
async def setup_climate_core_(var, config):
|
||||||
await setup_entity(var, config, "climate")
|
await setup_entity(var, config, "climate")
|
||||||
|
|
||||||
|
|||||||
@@ -50,21 +50,21 @@ void ClimateCall::perform() {
|
|||||||
const LogString *mode_s = climate_mode_to_string(*this->mode_);
|
const LogString *mode_s = climate_mode_to_string(*this->mode_);
|
||||||
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
|
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
|
||||||
}
|
}
|
||||||
if (this->custom_fan_mode_.has_value()) {
|
if (this->custom_fan_mode_ != nullptr) {
|
||||||
this->fan_mode_.reset();
|
this->fan_mode_.reset();
|
||||||
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
|
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_);
|
||||||
}
|
}
|
||||||
if (this->fan_mode_.has_value()) {
|
if (this->fan_mode_.has_value()) {
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_ = nullptr;
|
||||||
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
|
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
|
||||||
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
|
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
|
||||||
}
|
}
|
||||||
if (this->custom_preset_.has_value()) {
|
if (this->custom_preset_ != nullptr) {
|
||||||
this->preset_.reset();
|
this->preset_.reset();
|
||||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
|
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
|
||||||
}
|
}
|
||||||
if (this->preset_.has_value()) {
|
if (this->preset_.has_value()) {
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_ = nullptr;
|
||||||
const LogString *preset_s = climate_preset_to_string(*this->preset_);
|
const LogString *preset_s = climate_preset_to_string(*this->preset_);
|
||||||
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
|
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
|
||||||
}
|
}
|
||||||
@@ -96,11 +96,10 @@ void ClimateCall::validate_() {
|
|||||||
this->mode_.reset();
|
this->mode_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->custom_fan_mode_.has_value()) {
|
if (this->custom_fan_mode_ != nullptr) {
|
||||||
auto custom_fan_mode = *this->custom_fan_mode_;
|
if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) {
|
||||||
if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
|
ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_);
|
||||||
ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
|
this->custom_fan_mode_ = nullptr;
|
||||||
this->custom_fan_mode_.reset();
|
|
||||||
}
|
}
|
||||||
} else if (this->fan_mode_.has_value()) {
|
} else if (this->fan_mode_.has_value()) {
|
||||||
auto fan_mode = *this->fan_mode_;
|
auto fan_mode = *this->fan_mode_;
|
||||||
@@ -109,11 +108,10 @@ void ClimateCall::validate_() {
|
|||||||
this->fan_mode_.reset();
|
this->fan_mode_.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->custom_preset_.has_value()) {
|
if (this->custom_preset_ != nullptr) {
|
||||||
auto custom_preset = *this->custom_preset_;
|
if (!traits.supports_custom_preset(this->custom_preset_)) {
|
||||||
if (!traits.supports_custom_preset(custom_preset)) {
|
ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_);
|
||||||
ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
|
this->custom_preset_ = nullptr;
|
||||||
this->custom_preset_.reset();
|
|
||||||
}
|
}
|
||||||
} else if (this->preset_.has_value()) {
|
} else if (this->preset_.has_value()) {
|
||||||
auto preset = *this->preset_;
|
auto preset = *this->preset_;
|
||||||
@@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
|
|||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
|
||||||
this->fan_mode_ = fan_mode;
|
this->fan_mode_ = fan_mode;
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_ = nullptr;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) {
|
||||||
|
// Check if it's a standard enum mode first
|
||||||
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
|
||||||
if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
|
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) {
|
||||||
this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
|
||||||
return *this;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
|
// Find the matching pointer from parent climate device
|
||||||
this->custom_fan_mode_ = fan_mode;
|
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) {
|
||||||
|
this->custom_fan_mode_ = mode_ptr;
|
||||||
this->fan_mode_.reset();
|
this->fan_mode_.reset();
|
||||||
} else {
|
return *this;
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
|
|
||||||
}
|
}
|
||||||
|
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
||||||
if (fan_mode.has_value()) {
|
if (fan_mode.has_value()) {
|
||||||
this->set_fan_mode(fan_mode.value());
|
this->set_fan_mode(fan_mode.value());
|
||||||
@@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
|
|||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
|
||||||
this->preset_ = preset;
|
this->preset_ = preset;
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_ = nullptr;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
|
ClimateCall &ClimateCall::set_preset(const char *custom_preset) {
|
||||||
|
// Check if it's a standard enum preset first
|
||||||
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
|
||||||
if (str_equals_case_insensitive(preset, preset_entry.str)) {
|
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) {
|
||||||
this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
|
||||||
return *this;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (this->parent_->get_traits().supports_custom_preset(preset)) {
|
// Find the matching pointer from parent climate device
|
||||||
this->custom_preset_ = preset;
|
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) {
|
||||||
|
this->custom_preset_ = preset_ptr;
|
||||||
this->preset_.reset();
|
this->preset_.reset();
|
||||||
} else {
|
return *this;
|
||||||
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
|
|
||||||
}
|
}
|
||||||
|
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
|
||||||
if (preset.has_value()) {
|
if (preset.has_value()) {
|
||||||
this->set_preset(preset.value());
|
this->set_preset(preset.value());
|
||||||
@@ -287,8 +291,14 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
|
|||||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
|
||||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
|
||||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
|
||||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
|
||||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
|
optional<std::string> ClimateCall::get_custom_fan_mode() const {
|
||||||
|
return this->custom_fan_mode_ != nullptr ? std::string(this->custom_fan_mode_) : optional<std::string>{};
|
||||||
|
}
|
||||||
|
|
||||||
|
optional<std::string> ClimateCall::get_custom_preset() const {
|
||||||
|
return this->custom_preset_ != nullptr ? std::string(this->custom_preset_) : optional<std::string>{};
|
||||||
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
|
||||||
this->target_temperature_high_ = target_temperature_high;
|
this->target_temperature_high_ = target_temperature_high;
|
||||||
@@ -317,13 +327,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
|
|||||||
|
|
||||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
|
||||||
this->fan_mode_ = fan_mode;
|
this->fan_mode_ = fan_mode;
|
||||||
this->custom_fan_mode_.reset();
|
this->custom_fan_mode_ = nullptr;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
|
||||||
this->preset_ = preset;
|
this->preset_ = preset;
|
||||||
this->custom_preset_.reset();
|
this->custom_preset_ = nullptr;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,13 +392,13 @@ void Climate::save_state_() {
|
|||||||
state.uses_custom_fan_mode = false;
|
state.uses_custom_fan_mode = false;
|
||||||
state.fan_mode = this->fan_mode.value();
|
state.fan_mode = this->fan_mode.value();
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
|
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
|
||||||
state.uses_custom_fan_mode = true;
|
state.uses_custom_fan_mode = true;
|
||||||
const auto &supported = traits.get_supported_custom_fan_modes();
|
const auto &supported = traits.get_supported_custom_fan_modes();
|
||||||
// std::set has consistent order (lexicographic for strings)
|
// std::vector maintains insertion order
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
for (const auto &mode : supported) {
|
for (const char *mode : supported) {
|
||||||
if (mode == custom_fan_mode) {
|
if (strcmp(mode, this->custom_fan_mode_) == 0) {
|
||||||
state.custom_fan_mode = i;
|
state.custom_fan_mode = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -399,13 +409,13 @@ void Climate::save_state_() {
|
|||||||
state.uses_custom_preset = false;
|
state.uses_custom_preset = false;
|
||||||
state.preset = this->preset.value();
|
state.preset = this->preset.value();
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
|
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
|
||||||
state.uses_custom_preset = true;
|
state.uses_custom_preset = true;
|
||||||
const auto &supported = traits.get_supported_custom_presets();
|
const auto &supported = traits.get_supported_custom_presets();
|
||||||
// std::set has consistent order (lexicographic for strings)
|
// std::vector maintains insertion order
|
||||||
size_t i = 0;
|
size_t i = 0;
|
||||||
for (const auto &preset : supported) {
|
for (const char *preset : supported) {
|
||||||
if (preset == custom_preset) {
|
if (strcmp(preset, this->custom_preset_) == 0) {
|
||||||
state.custom_preset = i;
|
state.custom_preset = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -430,14 +440,14 @@ void Climate::publish_state() {
|
|||||||
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
|
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
|
||||||
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
|
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
|
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) {
|
||||||
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
|
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_presets() && this->preset.has_value()) {
|
if (traits.get_supports_presets() && this->preset.has_value()) {
|
||||||
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
|
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
|
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) {
|
||||||
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str());
|
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_);
|
||||||
}
|
}
|
||||||
if (traits.get_supports_swing_modes()) {
|
if (traits.get_supports_swing_modes()) {
|
||||||
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
|
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
|
||||||
@@ -527,7 +537,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
if (this->uses_custom_fan_mode) {
|
if (this->uses_custom_fan_mode) {
|
||||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||||
call.fan_mode_.reset();
|
call.fan_mode_.reset();
|
||||||
call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
|
||||||
}
|
}
|
||||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||||
call.set_fan_mode(this->fan_mode);
|
call.set_fan_mode(this->fan_mode);
|
||||||
@@ -535,7 +545,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
|||||||
if (this->uses_custom_preset) {
|
if (this->uses_custom_preset) {
|
||||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||||
call.preset_.reset();
|
call.preset_.reset();
|
||||||
call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
|
||||||
}
|
}
|
||||||
} else if (traits.supports_preset(this->preset)) {
|
} else if (traits.supports_preset(this->preset)) {
|
||||||
call.set_preset(this->preset);
|
call.set_preset(this->preset);
|
||||||
@@ -562,20 +572,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
|||||||
if (this->uses_custom_fan_mode) {
|
if (this->uses_custom_fan_mode) {
|
||||||
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
|
||||||
climate->fan_mode.reset();
|
climate->fan_mode.reset();
|
||||||
climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
|
climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode];
|
||||||
}
|
}
|
||||||
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
} else if (traits.supports_fan_mode(this->fan_mode)) {
|
||||||
climate->fan_mode = this->fan_mode;
|
climate->fan_mode = this->fan_mode;
|
||||||
climate->custom_fan_mode.reset();
|
climate->clear_custom_fan_mode_();
|
||||||
}
|
}
|
||||||
if (this->uses_custom_preset) {
|
if (this->uses_custom_preset) {
|
||||||
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
if (this->custom_preset < traits.get_supported_custom_presets().size()) {
|
||||||
climate->preset.reset();
|
climate->preset.reset();
|
||||||
climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
|
climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset];
|
||||||
}
|
}
|
||||||
} else if (traits.supports_preset(this->preset)) {
|
} else if (traits.supports_preset(this->preset)) {
|
||||||
climate->preset = this->preset;
|
climate->preset = this->preset;
|
||||||
climate->custom_preset.reset();
|
climate->clear_custom_preset_();
|
||||||
}
|
}
|
||||||
if (traits.supports_swing_mode(this->swing_mode)) {
|
if (traits.supports_swing_mode(this->swing_mode)) {
|
||||||
climate->swing_mode = this->swing_mode;
|
climate->swing_mode = this->swing_mode;
|
||||||
@@ -583,28 +593,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
|
|||||||
climate->publish_state();
|
climate->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
|
/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion.
|
||||||
bool is_changed = alt.has_value();
|
*
|
||||||
alt.reset();
|
* Climate devices have mutually exclusive mode pairs:
|
||||||
if (is_changed || dst != src) {
|
* - fan_mode (enum) vs custom_fan_mode_ (const char*)
|
||||||
dst = src;
|
* - preset (enum) vs custom_preset_ (const char*)
|
||||||
is_changed = true;
|
*
|
||||||
|
* Only one mode in each pair can be active at a time. This helper ensures setting a primary
|
||||||
|
* mode automatically clears its corresponding custom mode.
|
||||||
|
*
|
||||||
|
* Example state transitions:
|
||||||
|
* Before: custom_fan_mode_="Turbo", fan_mode=nullopt
|
||||||
|
* Call: set_fan_mode_(CLIMATE_FAN_HIGH)
|
||||||
|
* After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH
|
||||||
|
*
|
||||||
|
* @param primary The primary mode optional (fan_mode or preset)
|
||||||
|
* @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_)
|
||||||
|
* @param value The new primary mode value to set
|
||||||
|
* @return true if state changed, false if already set to this value
|
||||||
|
*/
|
||||||
|
template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) {
|
||||||
|
// Clear the custom mode (mutual exclusion)
|
||||||
|
bool changed = custom_ptr != nullptr;
|
||||||
|
custom_ptr = nullptr;
|
||||||
|
// Set the primary mode
|
||||||
|
if (changed || !primary.has_value() || primary.value() != value) {
|
||||||
|
primary = value;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
return is_changed;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion.
|
||||||
|
*
|
||||||
|
* This helper ensures setting a custom mode automatically clears its corresponding primary mode.
|
||||||
|
* It also validates that the custom mode exists in the device's supported modes (lifetime safety).
|
||||||
|
*
|
||||||
|
* Example state transitions:
|
||||||
|
* Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr
|
||||||
|
* Call: set_custom_fan_mode_("Turbo")
|
||||||
|
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
|
||||||
|
*
|
||||||
|
* Lifetime Safety:
|
||||||
|
* - found_ptr must come from traits.find_custom_*_mode_()
|
||||||
|
* - Only pointers found in traits are stored, ensuring they remain valid
|
||||||
|
* - Prevents dangling pointers from temporary strings
|
||||||
|
*
|
||||||
|
* @param custom_ptr Reference to the custom mode pointer to set
|
||||||
|
* @param primary The primary mode optional to clear
|
||||||
|
* @param found_ptr The validated pointer from traits (nullptr if not found)
|
||||||
|
* @param has_custom Whether a custom mode is currently active
|
||||||
|
* @return true if state changed, false otherwise
|
||||||
|
*/
|
||||||
|
template<typename T>
|
||||||
|
bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) {
|
||||||
|
if (found_ptr != nullptr) {
|
||||||
|
// Clear the primary mode (mutual exclusion)
|
||||||
|
bool changed = primary.has_value();
|
||||||
|
primary.reset();
|
||||||
|
// Set the custom mode (pointer is validated by caller from traits)
|
||||||
|
if (changed || custom_ptr != found_ptr) {
|
||||||
|
custom_ptr = found_ptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Mode not found in supported modes, clear it if currently set
|
||||||
|
if (has_custom) {
|
||||||
|
custom_ptr = nullptr;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Climate::set_fan_mode_(ClimateFanMode mode) {
|
bool Climate::set_fan_mode_(ClimateFanMode mode) {
|
||||||
return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
|
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Climate::set_custom_fan_mode_(const std::string &mode) {
|
bool Climate::set_custom_fan_mode_(const char *mode) {
|
||||||
return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
|
auto traits = this->get_traits();
|
||||||
|
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
|
||||||
|
this->has_custom_fan_mode());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
|
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
|
||||||
|
|
||||||
bool Climate::set_custom_preset_(const std::string &preset) {
|
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
|
||||||
return set_alternative(this->custom_preset, this->preset, preset);
|
|
||||||
|
bool Climate::set_custom_preset_(const char *preset) {
|
||||||
|
auto traits = this->get_traits();
|
||||||
|
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
|
||||||
|
this->has_custom_preset());
|
||||||
|
}
|
||||||
|
|
||||||
|
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
|
||||||
|
|
||||||
|
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
|
||||||
|
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *Climate::find_custom_preset_(const char *custom_preset) {
|
||||||
|
return this->get_traits().find_custom_preset_(custom_preset);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Climate::dump_traits_(const char *tag) {
|
void Climate::dump_traits_(const char *tag) {
|
||||||
@@ -656,8 +745,8 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||||
for (const std::string &s : traits.get_supported_custom_fan_modes())
|
for (const char *s : traits.get_supported_custom_fan_modes())
|
||||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
ESP_LOGCONFIG(tag, " - %s", s);
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_presets().empty()) {
|
if (!traits.get_supported_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||||
@@ -666,8 +755,8 @@ void Climate::dump_traits_(const char *tag) {
|
|||||||
}
|
}
|
||||||
if (!traits.get_supported_custom_presets().empty()) {
|
if (!traits.get_supported_custom_presets().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||||
for (const std::string &s : traits.get_supported_custom_presets())
|
for (const char *s : traits.get_supported_custom_presets())
|
||||||
ESP_LOGCONFIG(tag, " - %s", s.c_str());
|
ESP_LOGCONFIG(tag, " - %s", s);
|
||||||
}
|
}
|
||||||
if (!traits.get_supported_swing_modes().empty()) {
|
if (!traits.get_supported_swing_modes().empty()) {
|
||||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ class ClimateCall {
|
|||||||
ClimateCall &set_fan_mode(const std::string &fan_mode);
|
ClimateCall &set_fan_mode(const std::string &fan_mode);
|
||||||
/// Set the fan mode of the climate device based on a string.
|
/// Set the fan mode of the climate device based on a string.
|
||||||
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
|
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
|
||||||
|
/// Set the custom fan mode of the climate device.
|
||||||
|
ClimateCall &set_fan_mode(const char *custom_fan_mode);
|
||||||
/// Set the swing mode of the climate device.
|
/// Set the swing mode of the climate device.
|
||||||
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
|
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
|
||||||
/// Set the swing mode of the climate device.
|
/// Set the swing mode of the climate device.
|
||||||
@@ -91,6 +93,8 @@ class ClimateCall {
|
|||||||
ClimateCall &set_preset(const std::string &preset);
|
ClimateCall &set_preset(const std::string &preset);
|
||||||
/// Set the preset of the climate device based on a string.
|
/// Set the preset of the climate device based on a string.
|
||||||
ClimateCall &set_preset(optional<std::string> preset);
|
ClimateCall &set_preset(optional<std::string> preset);
|
||||||
|
/// Set the custom preset of the climate device.
|
||||||
|
ClimateCall &set_preset(const char *custom_preset);
|
||||||
|
|
||||||
void perform();
|
void perform();
|
||||||
|
|
||||||
@@ -103,8 +107,8 @@ class ClimateCall {
|
|||||||
const optional<ClimateFanMode> &get_fan_mode() const;
|
const optional<ClimateFanMode> &get_fan_mode() const;
|
||||||
const optional<ClimateSwingMode> &get_swing_mode() const;
|
const optional<ClimateSwingMode> &get_swing_mode() const;
|
||||||
const optional<ClimatePreset> &get_preset() const;
|
const optional<ClimatePreset> &get_preset() const;
|
||||||
const optional<std::string> &get_custom_fan_mode() const;
|
optional<std::string> get_custom_fan_mode() const;
|
||||||
const optional<std::string> &get_custom_preset() const;
|
optional<std::string> get_custom_preset() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void validate_();
|
void validate_();
|
||||||
@@ -118,8 +122,8 @@ class ClimateCall {
|
|||||||
optional<ClimateFanMode> fan_mode_;
|
optional<ClimateFanMode> fan_mode_;
|
||||||
optional<ClimateSwingMode> swing_mode_;
|
optional<ClimateSwingMode> swing_mode_;
|
||||||
optional<ClimatePreset> preset_;
|
optional<ClimatePreset> preset_;
|
||||||
optional<std::string> custom_fan_mode_;
|
const char *custom_fan_mode_{nullptr};
|
||||||
optional<std::string> custom_preset_;
|
const char *custom_preset_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Struct used to save the state of the climate device in restore memory.
|
/// Struct used to save the state of the climate device in restore memory.
|
||||||
@@ -212,6 +216,12 @@ class Climate : public EntityBase {
|
|||||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||||
|
|
||||||
|
/// Check if a custom fan mode is currently active.
|
||||||
|
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
|
||||||
|
|
||||||
|
/// Check if a custom preset is currently active.
|
||||||
|
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
|
||||||
|
|
||||||
/// The current temperature of the climate device, as reported from the integration.
|
/// The current temperature of the climate device, as reported from the integration.
|
||||||
float current_temperature{NAN};
|
float current_temperature{NAN};
|
||||||
|
|
||||||
@@ -238,12 +248,6 @@ class Climate : public EntityBase {
|
|||||||
/// The active preset of the climate device.
|
/// The active preset of the climate device.
|
||||||
optional<ClimatePreset> preset;
|
optional<ClimatePreset> preset;
|
||||||
|
|
||||||
/// The active custom fan mode of the climate device.
|
|
||||||
optional<std::string> custom_fan_mode;
|
|
||||||
|
|
||||||
/// The active custom preset mode of the climate device.
|
|
||||||
optional<std::string> custom_preset;
|
|
||||||
|
|
||||||
/// The active mode of the climate device.
|
/// The active mode of the climate device.
|
||||||
ClimateMode mode{CLIMATE_MODE_OFF};
|
ClimateMode mode{CLIMATE_MODE_OFF};
|
||||||
|
|
||||||
@@ -253,20 +257,37 @@ class Climate : public EntityBase {
|
|||||||
/// The active swing mode of the climate device.
|
/// The active swing mode of the climate device.
|
||||||
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
|
||||||
|
|
||||||
|
/// Get the active custom fan mode (read-only access).
|
||||||
|
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
|
||||||
|
|
||||||
|
/// Get the active custom preset (read-only access).
|
||||||
|
const char *get_custom_preset() const { return this->custom_preset_; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend ClimateCall;
|
friend ClimateCall;
|
||||||
|
friend struct ClimateDeviceRestoreState;
|
||||||
|
|
||||||
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
|
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
|
||||||
bool set_fan_mode_(ClimateFanMode mode);
|
bool set_fan_mode_(ClimateFanMode mode);
|
||||||
|
|
||||||
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
|
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
|
||||||
bool set_custom_fan_mode_(const std::string &mode);
|
bool set_custom_fan_mode_(const char *mode);
|
||||||
|
/// Clear custom fan mode.
|
||||||
|
void clear_custom_fan_mode_();
|
||||||
|
|
||||||
/// Set preset. Reset custom preset. Return true if preset has been changed.
|
/// Set preset. Reset custom preset. Return true if preset has been changed.
|
||||||
bool set_preset_(ClimatePreset preset);
|
bool set_preset_(ClimatePreset preset);
|
||||||
|
|
||||||
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
|
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
|
||||||
bool set_custom_preset_(const std::string &preset);
|
bool set_custom_preset_(const char *preset);
|
||||||
|
/// Clear custom preset.
|
||||||
|
void clear_custom_preset_();
|
||||||
|
|
||||||
|
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
|
||||||
|
const char *find_custom_fan_mode_(const char *custom_fan_mode);
|
||||||
|
|
||||||
|
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
|
||||||
|
const char *find_custom_preset_(const char *custom_preset);
|
||||||
|
|
||||||
/** Get the default traits of this climate device.
|
/** Get the default traits of this climate device.
|
||||||
*
|
*
|
||||||
@@ -303,6 +324,21 @@ class Climate : public EntityBase {
|
|||||||
optional<float> visual_current_temperature_step_override_{};
|
optional<float> visual_current_temperature_step_override_{};
|
||||||
optional<float> visual_min_humidity_override_{};
|
optional<float> visual_min_humidity_override_{};
|
||||||
optional<float> visual_max_humidity_override_{};
|
optional<float> visual_max_humidity_override_{};
|
||||||
|
|
||||||
|
private:
|
||||||
|
/** The active custom fan mode (private - enforces use of safe setters).
|
||||||
|
*
|
||||||
|
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
|
||||||
|
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
|
||||||
|
*/
|
||||||
|
const char *custom_fan_mode_{nullptr};
|
||||||
|
|
||||||
|
/** The active custom preset (private - enforces use of safe setters).
|
||||||
|
*
|
||||||
|
* Points to an entry in traits.supported_custom_presets_ or nullptr.
|
||||||
|
* Use get_custom_preset() to read, set_custom_preset_() to modify.
|
||||||
|
*/
|
||||||
|
const char *custom_preset_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace climate
|
} // namespace climate
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace esphome {
|
|||||||
namespace climate {
|
namespace climate {
|
||||||
|
|
||||||
/// Enum for all modes a climate device can be in.
|
/// Enum for all modes a climate device can be in.
|
||||||
|
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
|
||||||
enum ClimateMode : uint8_t {
|
enum ClimateMode : uint8_t {
|
||||||
/// The climate device is off
|
/// The climate device is off
|
||||||
CLIMATE_MODE_OFF = 0,
|
CLIMATE_MODE_OFF = 0,
|
||||||
@@ -24,7 +25,7 @@ enum ClimateMode : uint8_t {
|
|||||||
* For example, the target temperature can be adjusted based on a schedule, or learned behavior.
|
* For example, the target temperature can be adjusted based on a schedule, or learned behavior.
|
||||||
* The target temperature can't be adjusted when in this mode.
|
* The target temperature can't be adjusted when in this mode.
|
||||||
*/
|
*/
|
||||||
CLIMATE_MODE_AUTO = 6
|
CLIMATE_MODE_AUTO = 6 // Update ClimateModeMask in climate_traits.h if adding values after this
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Enum for the current action of the climate device. Values match those of ClimateMode.
|
/// Enum for the current action of the climate device. Values match those of ClimateMode.
|
||||||
@@ -43,6 +44,7 @@ enum ClimateAction : uint8_t {
|
|||||||
CLIMATE_ACTION_FAN = 6,
|
CLIMATE_ACTION_FAN = 6,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
|
||||||
enum ClimateFanMode : uint8_t {
|
enum ClimateFanMode : uint8_t {
|
||||||
/// The fan mode is set to On
|
/// The fan mode is set to On
|
||||||
CLIMATE_FAN_ON = 0,
|
CLIMATE_FAN_ON = 0,
|
||||||
@@ -63,10 +65,11 @@ enum ClimateFanMode : uint8_t {
|
|||||||
/// The fan mode is set to Diffuse
|
/// The fan mode is set to Diffuse
|
||||||
CLIMATE_FAN_DIFFUSE = 8,
|
CLIMATE_FAN_DIFFUSE = 8,
|
||||||
/// The fan mode is set to Quiet
|
/// The fan mode is set to Quiet
|
||||||
CLIMATE_FAN_QUIET = 9,
|
CLIMATE_FAN_QUIET = 9, // Update ClimateFanModeMask in climate_traits.h if adding values after this
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Enum for all modes a climate swing can be in
|
/// Enum for all modes a climate swing can be in
|
||||||
|
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
|
||||||
enum ClimateSwingMode : uint8_t {
|
enum ClimateSwingMode : uint8_t {
|
||||||
/// The swing mode is set to Off
|
/// The swing mode is set to Off
|
||||||
CLIMATE_SWING_OFF = 0,
|
CLIMATE_SWING_OFF = 0,
|
||||||
@@ -75,10 +78,11 @@ enum ClimateSwingMode : uint8_t {
|
|||||||
/// The fan mode is set to Vertical
|
/// The fan mode is set to Vertical
|
||||||
CLIMATE_SWING_VERTICAL = 2,
|
CLIMATE_SWING_VERTICAL = 2,
|
||||||
/// The fan mode is set to Horizontal
|
/// The fan mode is set to Horizontal
|
||||||
CLIMATE_SWING_HORIZONTAL = 3,
|
CLIMATE_SWING_HORIZONTAL = 3, // Update ClimateSwingModeMask in climate_traits.h if adding values after this
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Enum for all preset modes
|
/// Enum for all preset modes
|
||||||
|
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
|
||||||
enum ClimatePreset : uint8_t {
|
enum ClimatePreset : uint8_t {
|
||||||
/// No preset is active
|
/// No preset is active
|
||||||
CLIMATE_PRESET_NONE = 0,
|
CLIMATE_PRESET_NONE = 0,
|
||||||
@@ -95,7 +99,7 @@ enum ClimatePreset : uint8_t {
|
|||||||
/// Device is prepared for sleep
|
/// Device is prepared for sleep
|
||||||
CLIMATE_PRESET_SLEEP = 6,
|
CLIMATE_PRESET_SLEEP = 6,
|
||||||
/// Device is reacting to activity (e.g., movement sensors)
|
/// Device is reacting to activity (e.g., movement sensors)
|
||||||
CLIMATE_PRESET_ACTIVITY = 7,
|
CLIMATE_PRESET_ACTIVITY = 7, // Update ClimatePresetMask in climate_traits.h if adding values after this
|
||||||
};
|
};
|
||||||
|
|
||||||
enum ClimateFeature : uint32_t {
|
enum ClimateFeature : uint32_t {
|
||||||
|
|||||||
@@ -1,19 +1,43 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <set>
|
#include <cstring>
|
||||||
|
#include <vector>
|
||||||
#include "climate_mode.h"
|
#include "climate_mode.h"
|
||||||
|
#include "esphome/core/finite_set_mask.h"
|
||||||
#include "esphome/core/helpers.h"
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
#ifdef USE_API
|
|
||||||
namespace api {
|
|
||||||
class APIConnection;
|
|
||||||
} // namespace api
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace climate {
|
namespace climate {
|
||||||
|
|
||||||
|
// Type aliases for climate enum bitmasks
|
||||||
|
// These replace std::set<EnumType> to eliminate red-black tree overhead
|
||||||
|
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
|
||||||
|
// Bitmask size is automatically calculated from the last enum value
|
||||||
|
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
|
||||||
|
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
|
||||||
|
using ClimateSwingModeMask =
|
||||||
|
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
|
||||||
|
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
|
||||||
|
|
||||||
|
// Lightweight linear search for small vectors (1-20 items) of const char* pointers
|
||||||
|
// Avoids std::find template overhead
|
||||||
|
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) {
|
||||||
|
for (const char *item : vec) {
|
||||||
|
if (strcmp(item, value) == 0)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and return matching pointer from vector, or nullptr if not found
|
||||||
|
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
|
||||||
|
for (const char *item : vec) {
|
||||||
|
if (strcmp(item, value) == 0)
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
/** This class contains all static data for climate devices.
|
/** This class contains all static data for climate devices.
|
||||||
*
|
*
|
||||||
* All climate devices must support these features:
|
* All climate devices must support these features:
|
||||||
@@ -41,7 +65,11 @@ namespace climate {
|
|||||||
* - temperature step - the step with which to increase/decrease target temperature.
|
* - temperature step - the step with which to increase/decrease target temperature.
|
||||||
* This also affects with how many decimal places the temperature is shown
|
* This also affects with how many decimal places the temperature is shown
|
||||||
*/
|
*/
|
||||||
|
class Climate; // Forward declaration
|
||||||
|
|
||||||
class ClimateTraits {
|
class ClimateTraits {
|
||||||
|
friend class Climate; // Allow Climate to access protected find methods
|
||||||
|
|
||||||
public:
|
public:
|
||||||
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
|
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
|
||||||
uint32_t get_feature_flags() const { return this->feature_flags_; }
|
uint32_t get_feature_flags() const { return this->feature_flags_; }
|
||||||
@@ -107,48 +135,74 @@ class ClimateTraits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
|
void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
|
||||||
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
|
||||||
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
|
||||||
const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
|
const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
|
||||||
|
|
||||||
void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
|
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
|
||||||
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
|
||||||
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
|
|
||||||
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
|
||||||
bool get_supports_fan_modes() const {
|
bool get_supports_fan_modes() const {
|
||||||
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
|
||||||
}
|
}
|
||||||
const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
|
||||||
|
|
||||||
void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
|
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) {
|
||||||
this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
|
this->supported_custom_fan_modes_ = modes;
|
||||||
|
}
|
||||||
|
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) {
|
||||||
|
this->supported_custom_fan_modes_ = modes;
|
||||||
|
}
|
||||||
|
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
|
||||||
|
this->supported_custom_fan_modes_.assign(modes, modes + N);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||||
|
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
|
||||||
|
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
|
||||||
|
|
||||||
|
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
|
||||||
|
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
|
||||||
|
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
|
||||||
}
|
}
|
||||||
const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
|
|
||||||
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
|
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
|
||||||
return this->supported_custom_fan_modes_.count(custom_fan_mode);
|
return this->supports_custom_fan_mode(custom_fan_mode.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
|
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
|
||||||
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
|
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
|
||||||
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
|
|
||||||
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
|
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
|
||||||
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
|
bool get_supports_presets() const { return !this->supported_presets_.empty(); }
|
||||||
const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
|
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
|
||||||
|
|
||||||
void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
|
void set_supported_custom_presets(std::initializer_list<const char *> presets) {
|
||||||
this->supported_custom_presets_ = std::move(supported_custom_presets);
|
this->supported_custom_presets_ = presets;
|
||||||
|
}
|
||||||
|
void set_supported_custom_presets(const std::vector<const char *> &presets) {
|
||||||
|
this->supported_custom_presets_ = presets;
|
||||||
|
}
|
||||||
|
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
|
||||||
|
this->supported_custom_presets_.assign(presets, presets + N);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||||
|
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
|
||||||
|
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
|
||||||
|
|
||||||
|
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
|
||||||
|
bool supports_custom_preset(const char *custom_preset) const {
|
||||||
|
return vector_contains(this->supported_custom_presets_, custom_preset);
|
||||||
}
|
}
|
||||||
const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
|
|
||||||
bool supports_custom_preset(const std::string &custom_preset) const {
|
bool supports_custom_preset(const std::string &custom_preset) const {
|
||||||
return this->supported_custom_presets_.count(custom_preset);
|
return this->supports_custom_preset(custom_preset.c_str());
|
||||||
}
|
}
|
||||||
|
|
||||||
void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
|
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
|
||||||
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
|
||||||
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
|
||||||
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
|
||||||
const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
|
||||||
|
|
||||||
float get_visual_min_temperature() const { return this->visual_min_temperature_; }
|
float get_visual_min_temperature() const { return this->visual_min_temperature_; }
|
||||||
void set_visual_min_temperature(float visual_min_temperature) {
|
void set_visual_min_temperature(float visual_min_temperature) {
|
||||||
@@ -179,23 +233,6 @@ class ClimateTraits {
|
|||||||
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
|
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#ifdef USE_API
|
|
||||||
// The API connection is a friend class to access internal methods
|
|
||||||
friend class api::APIConnection;
|
|
||||||
// These methods return references to internal data structures.
|
|
||||||
// They are used by the API to avoid copying data when encoding messages.
|
|
||||||
// Warning: Do not use these methods outside of the API connection code.
|
|
||||||
// They return references to internal data that can be invalidated.
|
|
||||||
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
|
|
||||||
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
|
|
||||||
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
|
|
||||||
return this->supported_custom_fan_modes_;
|
|
||||||
}
|
|
||||||
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
|
|
||||||
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
|
|
||||||
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
void set_mode_support_(climate::ClimateMode mode, bool supported) {
|
void set_mode_support_(climate::ClimateMode mode, bool supported) {
|
||||||
if (supported) {
|
if (supported) {
|
||||||
this->supported_modes_.insert(mode);
|
this->supported_modes_.insert(mode);
|
||||||
@@ -218,6 +255,18 @@ class ClimateTraits {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
|
||||||
|
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
|
||||||
|
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
|
||||||
|
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
|
||||||
|
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
|
||||||
|
const char *find_custom_preset_(const char *custom_preset) const {
|
||||||
|
return vector_find(this->supported_custom_presets_, custom_preset);
|
||||||
|
}
|
||||||
|
|
||||||
uint32_t feature_flags_{0};
|
uint32_t feature_flags_{0};
|
||||||
float visual_min_temperature_{10};
|
float visual_min_temperature_{10};
|
||||||
float visual_max_temperature_{30};
|
float visual_max_temperature_{30};
|
||||||
@@ -226,12 +275,21 @@ class ClimateTraits {
|
|||||||
float visual_min_humidity_{30};
|
float visual_min_humidity_{30};
|
||||||
float visual_max_humidity_{99};
|
float visual_max_humidity_{99};
|
||||||
|
|
||||||
std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
|
climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
|
||||||
std::set<climate::ClimateFanMode> supported_fan_modes_;
|
climate::ClimateFanModeMask supported_fan_modes_;
|
||||||
std::set<climate::ClimateSwingMode> supported_swing_modes_;
|
climate::ClimateSwingModeMask supported_swing_modes_;
|
||||||
std::set<climate::ClimatePreset> supported_presets_;
|
climate::ClimatePresetMask supported_presets_;
|
||||||
std::set<std::string> supported_custom_fan_modes_;
|
|
||||||
std::set<std::string> supported_custom_presets_;
|
/** Custom mode storage using const char* pointers to eliminate std::string overhead.
|
||||||
|
*
|
||||||
|
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
|
||||||
|
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
|
||||||
|
* - Static const data: static const char* MODE = "Eco";
|
||||||
|
*
|
||||||
|
* Climate class setters validate pointers are from these vectors before storing.
|
||||||
|
*/
|
||||||
|
std::vector<const char *> supported_custom_fan_modes_;
|
||||||
|
std::vector<const char *> supported_custom_presets_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace climate
|
} // namespace climate
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from esphome import core
|
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components import climate, remote_base, sensor
|
from esphome.components import climate, remote_base, sensor
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
|
||||||
from esphome.cpp_generator import MockObjClass
|
from esphome.cpp_generator import MockObjClass
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
def deprecated_schema_constant(config):
|
|
||||||
type: str = "unknown"
|
|
||||||
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
|
|
||||||
type = str(id.type).split("::", maxsplit=1)[0]
|
|
||||||
_LOGGER.warning(
|
|
||||||
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
|
|
||||||
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
|
|
||||||
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
|
|
||||||
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
|
|
||||||
"Component using this schema: %s",
|
|
||||||
type,
|
|
||||||
)
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
|
|
||||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
|
|
||||||
|
|
||||||
|
|
||||||
async def register_climate_ir(var, config):
|
async def register_climate_ir(var, config):
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await remote_base.register_transmittable(var, config)
|
await remote_base.register_transmittable(var, config)
|
||||||
|
|||||||
@@ -24,16 +24,18 @@ class ClimateIR : public Component,
|
|||||||
public remote_base::RemoteTransmittable {
|
public remote_base::RemoteTransmittable {
|
||||||
public:
|
public:
|
||||||
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
|
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
|
||||||
bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
|
bool supports_dry = false, bool supports_fan_only = false,
|
||||||
std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) {
|
climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
|
||||||
|
climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
|
||||||
|
climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
|
||||||
this->minimum_temperature_ = minimum_temperature;
|
this->minimum_temperature_ = minimum_temperature;
|
||||||
this->maximum_temperature_ = maximum_temperature;
|
this->maximum_temperature_ = maximum_temperature;
|
||||||
this->temperature_step_ = temperature_step;
|
this->temperature_step_ = temperature_step;
|
||||||
this->supports_dry_ = supports_dry;
|
this->supports_dry_ = supports_dry;
|
||||||
this->supports_fan_only_ = supports_fan_only;
|
this->supports_fan_only_ = supports_fan_only;
|
||||||
this->fan_modes_ = std::move(fan_modes);
|
this->fan_modes_ = fan_modes;
|
||||||
this->swing_modes_ = std::move(swing_modes);
|
this->swing_modes_ = swing_modes;
|
||||||
this->presets_ = std::move(presets);
|
this->presets_ = presets;
|
||||||
}
|
}
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
|
|||||||
bool supports_heat_{true};
|
bool supports_heat_{true};
|
||||||
bool supports_dry_{false};
|
bool supports_dry_{false};
|
||||||
bool supports_fan_only_{false};
|
bool supports_fan_only_{false};
|
||||||
std::set<climate::ClimateFanMode> fan_modes_ = {};
|
climate::ClimateFanModeMask fan_modes_{};
|
||||||
std::set<climate::ClimateSwingMode> swing_modes_ = {};
|
climate::ClimateSwingModeMask swing_modes_{};
|
||||||
std::set<climate::ClimatePreset> presets_ = {};
|
climate::ClimatePresetMask presets_{};
|
||||||
|
|
||||||
sensor::Sensor *sensor_{nullptr};
|
sensor::Sensor *sensor_{nullptr};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ void CopyFan::setup() {
|
|||||||
this->oscillating = source_->oscillating;
|
this->oscillating = source_->oscillating;
|
||||||
this->speed = source_->speed;
|
this->speed = source_->speed;
|
||||||
this->direction = source_->direction;
|
this->direction = source_->direction;
|
||||||
this->preset_mode = source_->preset_mode;
|
this->set_preset_mode_(source_->get_preset_mode());
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ void CopyFan::setup() {
|
|||||||
this->oscillating = source_->oscillating;
|
this->oscillating = source_->oscillating;
|
||||||
this->speed = source_->speed;
|
this->speed = source_->speed;
|
||||||
this->direction = source_->direction;
|
this->direction = source_->direction;
|
||||||
this->preset_mode = source_->preset_mode;
|
this->set_preset_mode_(source_->get_preset_mode());
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
|
|||||||
call2.set_speed(*call.get_speed());
|
call2.set_speed(*call.get_speed());
|
||||||
if (call.get_direction().has_value())
|
if (call.get_direction().has_value())
|
||||||
call2.set_direction(*call.get_direction());
|
call2.set_direction(*call.get_direction());
|
||||||
if (!call.get_preset_mode().empty())
|
if (call.has_preset_mode())
|
||||||
call2.set_preset_mode(call.get_preset_mode());
|
call2.set_preset_mode(call.get_preset_mode());
|
||||||
call2.perform();
|
call2.perform();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ namespace copy {
|
|||||||
static const char *const TAG = "copy.select";
|
static const char *const TAG = "copy.select";
|
||||||
|
|
||||||
void CopySelect::setup() {
|
void CopySelect::setup() {
|
||||||
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
|
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); });
|
||||||
|
|
||||||
traits.set_options(source_->traits.get_options());
|
traits.set_options(source_->traits.get_options());
|
||||||
|
|
||||||
if (source_->has_state())
|
if (source_->has_state())
|
||||||
this->publish_state(source_->state);
|
this->publish_state(source_->active_index().value());
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
|
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
|
||||||
|
|
||||||
void CopySelect::control(const std::string &value) {
|
void CopySelect::control(size_t index) {
|
||||||
auto call = source_->make_call();
|
auto call = source_->make_call();
|
||||||
call.set_option(value);
|
call.set_index(index);
|
||||||
call.perform();
|
call.perform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component {
|
|||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void control(const std::string &value) override;
|
void control(size_t index) override;
|
||||||
|
|
||||||
select::Select *source_;
|
select::Select *source_;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -151,11 +151,6 @@ def cover_schema(
|
|||||||
return _COVER_SCHEMA.extend(schema)
|
return _COVER_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
COVER_SCHEMA = cover_schema(Cover)
|
|
||||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_cover_core_(var, config):
|
async def setup_cover_core_(var, config):
|
||||||
await setup_entity(var, config, "cover")
|
await setup_entity(var, config, "cover")
|
||||||
|
|
||||||
|
|||||||
@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
|
|||||||
this->mode = climate::CLIMATE_MODE_AUTO;
|
this->mode = climate::CLIMATE_MODE_AUTO;
|
||||||
this->action = climate::CLIMATE_ACTION_COOLING;
|
this->action = climate::CLIMATE_ACTION_COOLING;
|
||||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||||
this->custom_preset = {"My Preset"};
|
this->set_custom_preset_("My Preset");
|
||||||
break;
|
break;
|
||||||
case DemoClimateType::TYPE_3:
|
case DemoClimateType::TYPE_3:
|
||||||
this->current_temperature = 21.5;
|
this->current_temperature = 21.5;
|
||||||
this->target_temperature_low = 21.0;
|
this->target_temperature_low = 21.0;
|
||||||
this->target_temperature_high = 22.5;
|
this->target_temperature_high = 22.5;
|
||||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||||
this->custom_fan_mode = {"Auto Low"};
|
this->set_custom_fan_mode_("Auto Low");
|
||||||
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
|
||||||
this->preset = climate::CLIMATE_PRESET_AWAY;
|
this->set_preset_(climate::CLIMATE_PRESET_AWAY);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
@@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component {
|
|||||||
this->target_temperature_high = *call.get_target_temperature_high();
|
this->target_temperature_high = *call.get_target_temperature_high();
|
||||||
}
|
}
|
||||||
if (call.get_fan_mode().has_value()) {
|
if (call.get_fan_mode().has_value()) {
|
||||||
this->fan_mode = *call.get_fan_mode();
|
this->set_fan_mode_(*call.get_fan_mode());
|
||||||
this->custom_fan_mode.reset();
|
|
||||||
}
|
}
|
||||||
if (call.get_swing_mode().has_value()) {
|
if (call.get_swing_mode().has_value()) {
|
||||||
this->swing_mode = *call.get_swing_mode();
|
this->swing_mode = *call.get_swing_mode();
|
||||||
}
|
}
|
||||||
if (call.get_custom_fan_mode().has_value()) {
|
if (call.get_custom_fan_mode().has_value()) {
|
||||||
this->custom_fan_mode = *call.get_custom_fan_mode();
|
this->set_custom_fan_mode_(call.get_custom_fan_mode()->c_str());
|
||||||
this->fan_mode.reset();
|
|
||||||
}
|
}
|
||||||
if (call.get_preset().has_value()) {
|
if (call.get_preset().has_value()) {
|
||||||
this->preset = *call.get_preset();
|
this->set_preset_(*call.get_preset());
|
||||||
this->custom_preset.reset();
|
|
||||||
}
|
}
|
||||||
if (call.get_custom_preset().has_value()) {
|
if (call.get_custom_preset().has_value()) {
|
||||||
this->custom_preset = *call.get_custom_preset();
|
this->set_custom_preset_(call.get_custom_preset()->c_str());
|
||||||
this->preset.reset();
|
|
||||||
}
|
}
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
|
|||||||
result = this->value_getter_.value()(this);
|
result = this->value_getter_.value()(this);
|
||||||
} else {
|
} else {
|
||||||
if (this->select_var_ != nullptr) {
|
if (this->select_var_ != nullptr) {
|
||||||
result = this->select_var_->state;
|
result = this->select_var_->current_option();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
#include "e131_addressable_light_effect.h"
|
#include "e131_addressable_light_effect.h"
|
||||||
#include "esphome/core/log.h"
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace e131 {
|
namespace e131 {
|
||||||
|
|
||||||
@@ -76,14 +78,14 @@ void E131Component::loop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
||||||
if (light_effects_.count(light_effect)) {
|
if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||||
light_effect->get_last_universe());
|
light_effect->get_last_universe());
|
||||||
|
|
||||||
light_effects_.insert(light_effect);
|
light_effects_.push_back(light_effect);
|
||||||
|
|
||||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||||
join_(universe);
|
join_(universe);
|
||||||
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
|
||||||
if (!light_effects_.count(light_effect)) {
|
auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
|
||||||
|
if (it == light_effects_.end()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
|
||||||
light_effect->get_last_universe());
|
light_effect->get_last_universe());
|
||||||
|
|
||||||
light_effects_.erase(light_effect);
|
// Swap with last element and pop for O(1) removal (order doesn't matter)
|
||||||
|
*it = light_effects_.back();
|
||||||
|
light_effects_.pop_back();
|
||||||
|
|
||||||
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
|
||||||
leave_(universe);
|
leave_(universe);
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
#include <cinttypes>
|
#include <cinttypes>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <set>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
|
|||||||
|
|
||||||
E131ListenMethod listen_method_{E131_MULTICAST};
|
E131ListenMethod listen_method_{E131_MULTICAST};
|
||||||
std::unique_ptr<socket::Socket> socket_;
|
std::unique_ptr<socket::Socket> socket_;
|
||||||
std::set<E131AddressableLightEffect *> light_effects_;
|
std::vector<E131AddressableLightEffect *> light_effects_;
|
||||||
std::map<int, int> universe_consumers_;
|
std::map<int, int> universe_consumers_;
|
||||||
std::map<int, E131Packet> universe_packets_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace e131
|
} // namespace e131
|
||||||
|
|||||||
@@ -304,9 +304,13 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
|||||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
|
||||||
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
# format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
|
||||||
# a PIO platformio/framework-espidf value
|
# a PIO platformio/framework-espidf value
|
||||||
|
if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
|
||||||
|
ext = "tar.xz"
|
||||||
|
else:
|
||||||
|
ext = "zip"
|
||||||
if release:
|
if release:
|
||||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
|
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
|
||||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
|
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
|
||||||
|
|
||||||
|
|
||||||
def _is_framework_url(source: str) -> str:
|
def _is_framework_url(source: str) -> str:
|
||||||
@@ -355,6 +359,7 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
|
|||||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
|
||||||
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
|
cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
|
||||||
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
|
cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
|
||||||
|
cv.Version(5, 4, 3): cv.Version(55, 3, 32),
|
||||||
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
|
||||||
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
|
||||||
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
|
cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
|
||||||
|
|||||||
@@ -96,7 +96,11 @@ void loop_task(void *pv_params) {
|
|||||||
|
|
||||||
extern "C" void app_main() {
|
extern "C" void app_main() {
|
||||||
esp32::setup_preferences();
|
esp32::setup_preferences();
|
||||||
|
#if CONFIG_FREERTOS_UNICORE
|
||||||
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
|
xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
|
||||||
|
#else
|
||||||
|
xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif // USE_ESP_IDF
|
#endif // USE_ESP_IDF
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
|
|||||||
// - 3 bytes for members below
|
// - 3 bytes for members below
|
||||||
// - 1 byte padding for alignment
|
// - 1 byte padding for alignment
|
||||||
// - 4 bytes for vtable pointer
|
// - 4 bytes for vtable pointer
|
||||||
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
uint8_t pin_; // GPIO pin number (0-255, actual max ~54 on ESP32)
|
||||||
gpio::Flags flags_; // GPIO flags (1 byte)
|
gpio::Flags flags_{}; // GPIO flags (1 byte)
|
||||||
struct PinFlags {
|
struct PinFlags {
|
||||||
uint8_t inverted : 1; // Invert pin logic (1 bit)
|
uint8_t inverted : 1; // Invert pin logic (1 bit)
|
||||||
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
|
uint8_t drive_strength : 2; // Drive strength 0-3 (2 bits)
|
||||||
uint8_t reserved : 5; // Reserved for future use (5 bits)
|
uint8_t reserved : 5; // Reserved for future use (5 bits)
|
||||||
} pin_flags_; // Total: 1 byte
|
} pin_flags_{}; // Total: 1 byte
|
||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
static bool isr_service_installed;
|
static bool isr_service_installed;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -223,7 +223,10 @@ async def esp32_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
|
||||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
# Only set if true to avoid bloating setup() function
|
||||||
|
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
|
||||||
|
if config[CONF_INVERTED]:
|
||||||
|
cg.add(var.set_inverted(True))
|
||||||
if CONF_DRIVE_STRENGTH in config:
|
if CONF_DRIVE_STRENGTH in config:
|
||||||
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
|
||||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from typing import Any
|
|||||||
|
|
||||||
from esphome import automation
|
from esphome import automation
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import socket
|
||||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -481,6 +482,14 @@ async def to_code(config):
|
|||||||
cg.add(var.set_name(name))
|
cg.add(var.set_name(name))
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# BLE uses 1 UDP socket for event notification to wake up main loop from select()
|
||||||
|
# This enables low-latency (~12μs) BLE event processing instead of waiting for
|
||||||
|
# select() timeout (0-16ms). The socket is created in ble_setup_() and used to
|
||||||
|
# wake lwip_select() when BLE events arrive from the BLE thread.
|
||||||
|
# Note: Called during config generation, socket is created at runtime. In practice,
|
||||||
|
# always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT.
|
||||||
|
socket.consume_sockets(1, "esp32_ble")(config)
|
||||||
|
|
||||||
# Define max connections for use in C++ code (e.g., ble_server.h)
|
# Define max connections for use in C++ code (e.g., ble_server.h)
|
||||||
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
|
||||||
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
|
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
|
||||||
|
|||||||
@@ -27,10 +27,34 @@ extern "C" {
|
|||||||
#include <esp32-hal-bt.h>
|
#include <esp32-hal-bt.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::esp32_ble {
|
namespace esphome::esp32_ble {
|
||||||
|
|
||||||
static const char *const TAG = "esp32_ble";
|
static const char *const TAG = "esp32_ble";
|
||||||
|
|
||||||
|
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
|
||||||
|
#define GAP_SCAN_COMPLETE_EVENTS \
|
||||||
|
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
|
||||||
|
|
||||||
|
#define GAP_ADV_COMPLETE_EVENTS \
|
||||||
|
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
|
||||||
|
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
|
||||||
|
|
||||||
|
#define GAP_SECURITY_EVENTS \
|
||||||
|
case ESP_GAP_BLE_AUTH_CMPL_EVT: \
|
||||||
|
case ESP_GAP_BLE_SEC_REQ_EVT: \
|
||||||
|
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
|
||||||
|
case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
|
||||||
|
case ESP_GAP_BLE_NC_REQ_EVT
|
||||||
|
|
||||||
void ESP32BLE::setup() {
|
void ESP32BLE::setup() {
|
||||||
global_ble = this;
|
global_ble = this;
|
||||||
if (!ble_pre_setup_()) {
|
if (!ble_pre_setup_()) {
|
||||||
@@ -277,10 +301,21 @@ bool ESP32BLE::ble_setup_() {
|
|||||||
// BLE takes some time to be fully set up, 200ms should be more than enough
|
// BLE takes some time to be fully set up, 200ms should be more than enough
|
||||||
delay(200); // NOLINT
|
delay(200); // NOLINT
|
||||||
|
|
||||||
|
// Set up notification socket to wake main loop for BLE events
|
||||||
|
// This enables low-latency (~12μs) event processing instead of waiting for select() timeout
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
this->setup_event_notification_();
|
||||||
|
#endif
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ESP32BLE::ble_dismantle_() {
|
bool ESP32BLE::ble_dismantle_() {
|
||||||
|
// Clean up notification socket first before dismantling BLE stack
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
this->cleanup_event_notification_();
|
||||||
|
#endif
|
||||||
|
|
||||||
esp_err_t err = esp_bluedroid_disable();
|
esp_err_t err = esp_bluedroid_disable();
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
|
||||||
@@ -378,6 +413,12 @@ void ESP32BLE::loop() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
// Drain any notification socket events first
|
||||||
|
// This clears the socket so it doesn't stay "ready" in subsequent select() calls
|
||||||
|
this->drain_event_notifications_();
|
||||||
|
#endif
|
||||||
|
|
||||||
BLEEvent *ble_event = this->ble_events_.pop();
|
BLEEvent *ble_event = this->ble_events_.pop();
|
||||||
while (ble_event != nullptr) {
|
while (ble_event != nullptr) {
|
||||||
switch (ble_event->type_) {
|
switch (ble_event->type_) {
|
||||||
@@ -418,60 +459,48 @@ void ESP32BLE::loop() {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
// Scan complete events
|
// Scan complete events
|
||||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
GAP_SCAN_COMPLETE_EVENTS:
|
||||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
|
||||||
// All three scan complete events have the same structure with just status
|
|
||||||
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
|
|
||||||
// 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
|
// Advertising complete events
|
||||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
GAP_ADV_COMPLETE_EVENTS:
|
||||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
||||||
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
|
// RSSI complete event
|
||||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
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
|
// Security events
|
||||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
GAP_SECURITY_EVENTS:
|
||||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
|
||||||
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
|
|
||||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
|
|
||||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
|
||||||
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
|
||||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
|
||||||
for (auto *gap_handler : this->gap_event_handlers_) {
|
{
|
||||||
gap_handler->gap_event_handler(
|
esp_ble_gap_cb_param_t *param;
|
||||||
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
|
// clang-format off
|
||||||
|
switch (gap_event) {
|
||||||
|
// All three scan complete events have the same structure with just status
|
||||||
|
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
|
||||||
|
// 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)
|
||||||
|
GAP_SCAN_COMPLETE_EVENTS:
|
||||||
|
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
|
||||||
|
break;
|
||||||
|
|
||||||
|
// All advertising complete events have the same structure with just status
|
||||||
|
GAP_ADV_COMPLETE_EVENTS:
|
||||||
|
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||||
|
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete);
|
||||||
|
break;
|
||||||
|
|
||||||
|
GAP_SECURITY_EVENTS:
|
||||||
|
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// clang-format on
|
||||||
|
// Dispatch to all registered handlers
|
||||||
|
for (auto *gap_handler : this->gap_event_handlers_) {
|
||||||
|
gap_handler->gap_event_handler(gap_event, param);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
@@ -551,23 +580,13 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
|||||||
// Queue GAP events that components need to handle
|
// Queue GAP events that components need to handle
|
||||||
// Scanning events - used by esp32_ble_tracker
|
// Scanning events - used by esp32_ble_tracker
|
||||||
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
case ESP_GAP_BLE_SCAN_RESULT_EVT:
|
||||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
GAP_SCAN_COMPLETE_EVENTS:
|
||||||
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
|
|
||||||
// Advertising events - used by esp32_ble_beacon and esp32_ble server
|
// Advertising events - used by esp32_ble_beacon and esp32_ble server
|
||||||
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
|
GAP_ADV_COMPLETE_EVENTS:
|
||||||
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
|
|
||||||
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
|
|
||||||
// Connection events - used by ble_client
|
// Connection events - used by ble_client
|
||||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||||
// Security events - used by ble_client and bluetooth_proxy
|
// Security events - used by ble_client and bluetooth_proxy
|
||||||
case ESP_GAP_BLE_AUTH_CMPL_EVT:
|
GAP_SECURITY_EVENTS:
|
||||||
case ESP_GAP_BLE_SEC_REQ_EVT:
|
|
||||||
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
|
|
||||||
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
|
|
||||||
case ESP_GAP_BLE_NC_REQ_EVT:
|
|
||||||
enqueue_ble_event(event, param);
|
enqueue_ble_event(event, param);
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@@ -588,6 +607,10 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
|||||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||||
esp_ble_gatts_cb_param_t *param) {
|
esp_ble_gatts_cb_param_t *param) {
|
||||||
enqueue_ble_event(event, gatts_if, param);
|
enqueue_ble_event(event, gatts_if, param);
|
||||||
|
// Wake up main loop to process GATT event immediately
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
global_ble->notify_main_loop_();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -595,6 +618,10 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
|
|||||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||||
esp_ble_gattc_cb_param_t *param) {
|
esp_ble_gattc_cb_param_t *param) {
|
||||||
enqueue_ble_event(event, gattc_if, param);
|
enqueue_ble_event(event, gattc_if, param);
|
||||||
|
// Wake up main loop to process GATT event immediately
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
global_ble->notify_main_loop_();
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -634,6 +661,89 @@ void ESP32BLE::dump_config() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
void ESP32BLE::setup_event_notification_() {
|
||||||
|
// Create UDP socket for event notifications
|
||||||
|
this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
|
if (this->notify_fd_ < 0) {
|
||||||
|
ESP_LOGW(TAG, "Event socket create failed: %d", errno);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind to loopback with auto-assigned port
|
||||||
|
struct sockaddr_in addr = {};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
|
||||||
|
addr.sin_port = 0; // Auto-assign port
|
||||||
|
|
||||||
|
if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
|
||||||
|
ESP_LOGW(TAG, "Event socket bind failed: %d", errno);
|
||||||
|
lwip_close(this->notify_fd_);
|
||||||
|
this->notify_fd_ = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the assigned address and connect to it
|
||||||
|
// Connecting a UDP socket allows using send() instead of sendto() for better performance
|
||||||
|
struct sockaddr_in notify_addr;
|
||||||
|
socklen_t len = sizeof(notify_addr);
|
||||||
|
if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) {
|
||||||
|
ESP_LOGW(TAG, "Event socket address failed: %d", errno);
|
||||||
|
lwip_close(this->notify_fd_);
|
||||||
|
this->notify_fd_ = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to self (loopback) - allows using send() instead of sendto()
|
||||||
|
// After connect(), no need to store notify_addr - the socket remembers it
|
||||||
|
if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) {
|
||||||
|
ESP_LOGW(TAG, "Event socket connect failed: %d", errno);
|
||||||
|
lwip_close(this->notify_fd_);
|
||||||
|
this->notify_fd_ = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set non-blocking mode
|
||||||
|
int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0);
|
||||||
|
lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK);
|
||||||
|
|
||||||
|
// Register with application's select() loop
|
||||||
|
if (!App.register_socket_fd(this->notify_fd_)) {
|
||||||
|
ESP_LOGW(TAG, "Event socket register failed");
|
||||||
|
lwip_close(this->notify_fd_);
|
||||||
|
this->notify_fd_ = -1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Event socket ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32BLE::cleanup_event_notification_() {
|
||||||
|
if (this->notify_fd_ >= 0) {
|
||||||
|
App.unregister_socket_fd(this->notify_fd_);
|
||||||
|
lwip_close(this->notify_fd_);
|
||||||
|
this->notify_fd_ = -1;
|
||||||
|
ESP_LOGD(TAG, "Event socket closed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ESP32BLE::drain_event_notifications_() {
|
||||||
|
// Called from main loop to drain any pending notifications
|
||||||
|
// Must check is_socket_ready() to avoid blocking on empty socket
|
||||||
|
if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) {
|
||||||
|
char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE];
|
||||||
|
// Drain all pending notifications with non-blocking reads
|
||||||
|
// Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
|
||||||
|
// We control both ends of this loopback socket (always write 1 byte per event),
|
||||||
|
// so no error checking needed - any errors indicate catastrophic system failure
|
||||||
|
while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
|
||||||
|
// Just draining, no action needed - actual BLE events are already queued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||||
|
|
||||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
|
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
|
||||||
uint64_t u = 0;
|
uint64_t u = 0;
|
||||||
u |= uint64_t(address[0] & 0xFF) << 40;
|
u |= uint64_t(address[0] & 0xFF) << 40;
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
#include <esp_gattc_api.h>
|
#include <esp_gattc_api.h>
|
||||||
#include <esp_gatts_api.h>
|
#include <esp_gatts_api.h>
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
#include <lwip/sockets.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace esphome::esp32_ble {
|
namespace esphome::esp32_ble {
|
||||||
|
|
||||||
// Maximum size of the BLE event queue
|
// Maximum size of the BLE event queue
|
||||||
@@ -163,6 +167,13 @@ class ESP32BLE : public Component {
|
|||||||
void advertising_init_();
|
void advertising_init_();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
void setup_event_notification_(); // Create notification socket
|
||||||
|
void cleanup_event_notification_(); // Close and unregister socket
|
||||||
|
inline void notify_main_loop_(); // Wake up select() from BLE thread (hot path - inlined)
|
||||||
|
void drain_event_notifications_(); // Read pending notifications in main loop
|
||||||
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||||
|
|
||||||
@@ -197,6 +208,13 @@ class ESP32BLE : public Component {
|
|||||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
||||||
uint32_t advertising_cycle_time_{}; // 4 bytes
|
uint32_t advertising_cycle_time_{}; // 4 bytes
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
// Event notification socket for waking up main loop from BLE thread
|
||||||
|
// Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout
|
||||||
|
// Socket is connected during setup, allowing use of send() instead of sendto() for efficiency
|
||||||
|
int notify_fd_{-1}; // 4 bytes (file descriptor)
|
||||||
|
#endif
|
||||||
|
|
||||||
// 2-byte aligned members
|
// 2-byte aligned members
|
||||||
uint16_t appearance_{0}; // 2 bytes
|
uint16_t appearance_{0}; // 2 bytes
|
||||||
|
|
||||||
@@ -208,6 +226,29 @@ class ESP32BLE : public Component {
|
|||||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||||
extern ESP32BLE *global_ble;
|
extern ESP32BLE *global_ble;
|
||||||
|
|
||||||
|
#ifdef USE_SOCKET_SELECT_SUPPORT
|
||||||
|
// Inline implementations for hot-path functions
|
||||||
|
// These are called from BLE thread (notify) and main loop (drain) on every event
|
||||||
|
|
||||||
|
// Small buffer for draining notification bytes (1 byte sent per BLE event)
|
||||||
|
// Size allows draining multiple notifications per recvfrom() without wasting stack
|
||||||
|
static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16;
|
||||||
|
|
||||||
|
inline void ESP32BLE::notify_main_loop_() {
|
||||||
|
// Called from BLE thread context when events are queued
|
||||||
|
// Wakes up lwip_select() in main loop by writing to connected loopback socket
|
||||||
|
if (this->notify_fd_ >= 0) {
|
||||||
|
const char dummy = 1;
|
||||||
|
// Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
|
||||||
|
// No error checking needed: we control both ends of this loopback socket, and the
|
||||||
|
// BLE event is already queued. Notification is best-effort to reduce latency.
|
||||||
|
// This is safe to call from BLE thread - send() is thread-safe in lwip
|
||||||
|
// Socket is already connected to loopback address, so send() is faster than sendto()
|
||||||
|
lwip_send(this->notify_fd_, &dummy, 1, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif // USE_SOCKET_SELECT_SUPPORT
|
||||||
|
|
||||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
|
||||||
public:
|
public:
|
||||||
bool check(Ts... x) override { return global_ble->is_active(); }
|
bool check(Ts... x) override { return global_ble->is_active(); }
|
||||||
|
|||||||
@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
|
|||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
|
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
|
||||||
if isinstance(value, list):
|
if isinstance(value, list):
|
||||||
return cg.std_vector.template(cg.uint8)(value)
|
# Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
|
||||||
|
# This calls the set_value(std::initializer_list<uint8_t>) overload
|
||||||
|
return cg.ArrayInitializer(*value)
|
||||||
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
|
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
|
||||||
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
|
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
|
||||||
|
|
||||||
|
|||||||
@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
|
|||||||
|
|
||||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||||
|
|
||||||
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
|
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
|
||||||
xSemaphoreTake(this->set_value_lock_, 0L);
|
xSemaphoreTake(this->set_value_lock_, 0L);
|
||||||
this->value_ = buffer;
|
this->value_ = std::move(buffer);
|
||||||
xSemaphoreGive(this->set_value_lock_);
|
xSemaphoreGive(this->set_value_lock_);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
|
||||||
|
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
|
||||||
|
}
|
||||||
|
|
||||||
void BLECharacteristic::set_value(const std::string &buffer) {
|
void BLECharacteristic::set_value(const std::string &buffer) {
|
||||||
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
|
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); // Delegate to move overload
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLECharacteristic::notify() {
|
void BLECharacteristic::notify() {
|
||||||
|
|||||||
@@ -33,7 +33,8 @@ class BLECharacteristic {
|
|||||||
~BLECharacteristic();
|
~BLECharacteristic();
|
||||||
|
|
||||||
void set_value(ByteBuffer buffer);
|
void set_value(ByteBuffer buffer);
|
||||||
void set_value(const std::vector<uint8_t> &buffer);
|
void set_value(std::vector<uint8_t> &&buffer);
|
||||||
|
void set_value(std::initializer_list<uint8_t> data);
|
||||||
void set_value(const std::string &buffer);
|
void set_value(const std::string &buffer);
|
||||||
|
|
||||||
void set_broadcast_property(bool value);
|
void set_broadcast_property(bool value);
|
||||||
|
|||||||
@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
|
|||||||
this->state_ = CREATING;
|
this->state_ = CREATING;
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
|
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
|
||||||
size_t length = buffer.size();
|
|
||||||
|
|
||||||
|
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
|
||||||
|
|
||||||
|
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
|
||||||
if (length > this->value_.attr_max_len) {
|
if (length > this->value_.attr_max_len) {
|
||||||
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
|
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this->value_.attr_len = length;
|
this->value_.attr_len = length;
|
||||||
memcpy(this->value_.attr_value, buffer.data(), length);
|
memcpy(this->value_.attr_value, data, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ class BLEDescriptor {
|
|||||||
void do_create(BLECharacteristic *characteristic);
|
void do_create(BLECharacteristic *characteristic);
|
||||||
ESPBTUUID get_uuid() const { return this->uuid_; }
|
ESPBTUUID get_uuid() const { return this->uuid_; }
|
||||||
|
|
||||||
void set_value(std::vector<uint8_t> buffer);
|
void set_value(std::vector<uint8_t> &&buffer);
|
||||||
|
void set_value(std::initializer_list<uint8_t> data);
|
||||||
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||||
|
|
||||||
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
|
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
|
||||||
@@ -42,6 +43,8 @@ class BLEDescriptor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
void set_value_impl_(const uint8_t *data, size_t length);
|
||||||
|
|
||||||
BLECharacteristic *characteristic_{nullptr};
|
BLECharacteristic *characteristic_{nullptr};
|
||||||
ESPBTUUID uuid_;
|
ESPBTUUID uuid_;
|
||||||
uint16_t handle_{0xFFFF};
|
uint16_t handle_{0xFFFF};
|
||||||
|
|||||||
@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
|
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
|
||||||
this->rpc_response_->set_value(ByteBuffer::wrap(response));
|
this->rpc_response_->set_value(std::move(response));
|
||||||
if (this->state_ != improv::STATE_STOPPED)
|
if (this->state_ != improv::STATE_STOPPED)
|
||||||
this->rpc_response_->notify();
|
this->rpc_response_->notify();
|
||||||
}
|
}
|
||||||
@@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
|
this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
|
||||||
std::vector<uint8_t> data = improv::build_rpc_response(
|
std::vector<std::string>(url_strings, url_strings + url_count)));
|
||||||
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
|
|
||||||
this->send_response_(data);
|
|
||||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
|
|||||||
void set_state_(improv::State state, bool update_advertising = true);
|
void set_state_(improv::State state, bool update_advertising = true);
|
||||||
void set_error_(improv::Error error);
|
void set_error_(improv::Error error);
|
||||||
improv::State get_initial_state_() const;
|
improv::State get_initial_state_() const;
|
||||||
void send_response_(std::vector<uint8_t> &response);
|
void send_response_(std::vector<uint8_t> &&response);
|
||||||
void process_incoming_data_();
|
void process_incoming_data_();
|
||||||
void on_wifi_connect_timeout_();
|
void on_wifi_connect_timeout_();
|
||||||
void check_wifi_connection_();
|
void check_wifi_connection_();
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin {
|
|||||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esp8266
|
} // namespace esp8266
|
||||||
|
|||||||
@@ -165,7 +165,10 @@ async def esp8266_pin_to_code(config):
|
|||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
mode = config[CONF_MODE]
|
mode = config[CONF_MODE]
|
||||||
cg.add(var.set_pin(num))
|
cg.add(var.set_pin(num))
|
||||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
# Only set if true to avoid bloating setup() function
|
||||||
|
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
|
||||||
|
if config[CONF_INVERTED]:
|
||||||
|
cg.add(var.set_inverted(True))
|
||||||
cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
|
cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
|
||||||
if num < 16:
|
if num < 16:
|
||||||
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
|
initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
|
||||||
|
|||||||
@@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Acknowledge auth OK - 1 byte
|
// Acknowledge auth OK - 1 byte
|
||||||
buf[0] = ota::OTA_RESPONSE_AUTH_OK;
|
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
|
|
||||||
// Read size, 4 bytes MSB first
|
// Read size, 4 bytes MSB first
|
||||||
if (!this->readall_(buf, 4)) {
|
if (!this->readall_(buf, 4)) {
|
||||||
this->log_read_error_(LOG_STR("size"));
|
this->log_read_error_(LOG_STR("size"));
|
||||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||||
}
|
}
|
||||||
ota_size = 0;
|
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
|
||||||
for (uint8_t i = 0; i < 4; i++) {
|
(static_cast<size_t>(buf[2]) << 8) | buf[3];
|
||||||
ota_size <<= 8;
|
|
||||||
ota_size |= buf[i];
|
|
||||||
}
|
|
||||||
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
|
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
|
||||||
|
|
||||||
// Now that we've passed authentication and are actually
|
// Now that we've passed authentication and are actually
|
||||||
@@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
update_started = true;
|
update_started = true;
|
||||||
|
|
||||||
// Acknowledge prepare OK - 1 byte
|
// Acknowledge prepare OK - 1 byte
|
||||||
buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
|
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
|
|
||||||
// Read binary MD5, 32 bytes
|
// Read binary MD5, 32 bytes
|
||||||
if (!this->readall_(buf, 32)) {
|
if (!this->readall_(buf, 32)) {
|
||||||
@@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
this->backend_->set_update_md5(sbuf);
|
this->backend_->set_update_md5(sbuf);
|
||||||
|
|
||||||
// Acknowledge MD5 OK - 1 byte
|
// Acknowledge MD5 OK - 1 byte
|
||||||
buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
|
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
|
|
||||||
while (total < ota_size) {
|
while (total < ota_size) {
|
||||||
// TODO: timeout check
|
// TODO: timeout check
|
||||||
@@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
total += read;
|
total += read;
|
||||||
#if USE_OTA_VERSION == 2
|
#if USE_OTA_VERSION == 2
|
||||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||||
buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
|
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
size_acknowledged += OTA_BLOCK_SIZE;
|
size_acknowledged += OTA_BLOCK_SIZE;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acknowledge receive OK - 1 byte
|
// Acknowledge receive OK - 1 byte
|
||||||
buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
|
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
|
|
||||||
error_code = this->backend_->end();
|
error_code = this->backend_->end();
|
||||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||||
@@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Acknowledge Update end OK - 1 byte
|
// Acknowledge Update end OK - 1 byte
|
||||||
buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
|
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||||
this->writeall_(buf, 1);
|
|
||||||
|
|
||||||
// Read ACK
|
// Read ACK
|
||||||
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||||
@@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
|||||||
App.safe_reboot();
|
App.safe_reboot();
|
||||||
|
|
||||||
error:
|
error:
|
||||||
buf[0] = static_cast<uint8_t>(error_code);
|
this->write_byte_(static_cast<uint8_t>(error_code));
|
||||||
this->writeall_(buf, 1);
|
|
||||||
this->cleanup_connection_();
|
this->cleanup_connection_();
|
||||||
|
|
||||||
if (this->backend_ != nullptr && update_started) {
|
if (this->backend_ != nullptr && update_started) {
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
|
|||||||
#endif // USE_OTA_PASSWORD
|
#endif // USE_OTA_PASSWORD
|
||||||
bool readall_(uint8_t *buf, size_t len);
|
bool readall_(uint8_t *buf, size_t len);
|
||||||
bool writeall_(const uint8_t *buf, size_t len);
|
bool writeall_(const uint8_t *buf, size_t len);
|
||||||
|
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||||
|
|
||||||
bool try_read_(size_t to_read, const LogString *desc);
|
bool try_read_(size_t to_read, const LogString *desc);
|
||||||
bool try_write_(size_t to_write, const LogString *desc);
|
bool try_write_(size_t to_write, const LogString *desc);
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
|
|||||||
VARIANT_ESP32S2,
|
VARIANT_ESP32S2,
|
||||||
VARIANT_ESP32S3,
|
VARIANT_ESP32S3,
|
||||||
)
|
)
|
||||||
from esphome.components.network import IPAddress
|
from esphome.components.network import ip_address_literal
|
||||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -320,11 +320,11 @@ def _final_validate_spi(config):
|
|||||||
def manual_ip(config):
|
def manual_ip(config):
|
||||||
return cg.StructInitializer(
|
return cg.StructInitializer(
|
||||||
ManualIP,
|
ManualIP,
|
||||||
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
|
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
|
||||||
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
|
("gateway", ip_address_literal(config[CONF_GATEWAY])),
|
||||||
("subnet", IPAddress(str(config[CONF_SUBNET]))),
|
("subnet", ip_address_literal(config[CONF_SUBNET])),
|
||||||
("dns1", IPAddress(str(config[CONF_DNS1]))),
|
("dns1", ip_address_literal(config[CONF_DNS1])),
|
||||||
("dns2", IPAddress(str(config[CONF_DNS2]))),
|
("dns2", ip_address_literal(config[CONF_DNS2])),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -85,11 +85,6 @@ def event_schema(
|
|||||||
return _EVENT_SCHEMA.extend(schema)
|
return _EVENT_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
EVENT_SCHEMA = event_schema()
|
|
||||||
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
|
|
||||||
|
|
||||||
|
|
||||||
async def setup_event_core_(var, config, *, event_types: list[str]):
|
async def setup_event_core_(var, config, *, event_types: list[str]):
|
||||||
await setup_entity(var, config, "event")
|
await setup_entity(var, config, "event")
|
||||||
|
|
||||||
|
|||||||
@@ -189,10 +189,6 @@ def fan_schema(
|
|||||||
return _FAN_SCHEMA.extend(schema)
|
return _FAN_SCHEMA.extend(schema)
|
||||||
|
|
||||||
|
|
||||||
# Remove before 2025.11.0
|
|
||||||
FAN_SCHEMA = fan_schema(Fan)
|
|
||||||
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
|
|
||||||
|
|
||||||
_PRESET_MODES_SCHEMA = cv.All(
|
_PRESET_MODES_SCHEMA = cv.All(
|
||||||
cv.ensure_list(cv.string_strict),
|
cv.ensure_list(cv.string_strict),
|
||||||
cv.Length(min=1),
|
cv.Length(min=1),
|
||||||
|
|||||||
@@ -212,18 +212,18 @@ class FanPresetSetTrigger : public Trigger<std::string> {
|
|||||||
public:
|
public:
|
||||||
FanPresetSetTrigger(Fan *state) {
|
FanPresetSetTrigger(Fan *state) {
|
||||||
state->add_on_state_callback([this, state]() {
|
state->add_on_state_callback([this, state]() {
|
||||||
auto preset_mode = state->preset_mode;
|
const auto *preset_mode = state->get_preset_mode();
|
||||||
auto should_trigger = preset_mode != this->last_preset_mode_;
|
auto should_trigger = preset_mode != this->last_preset_mode_;
|
||||||
this->last_preset_mode_ = preset_mode;
|
this->last_preset_mode_ = preset_mode;
|
||||||
if (should_trigger) {
|
if (should_trigger && preset_mode != nullptr) {
|
||||||
this->trigger(preset_mode);
|
this->trigger(preset_mode);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this->last_preset_mode_ = state->preset_mode;
|
this->last_preset_mode_ = state->get_preset_mode();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string last_preset_mode_;
|
const char *last_preset_mode_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fan
|
} // namespace fan
|
||||||
|
|||||||
@@ -17,6 +17,27 @@ const LogString *fan_direction_to_string(FanDirection direction) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { return this->set_preset_mode(preset_mode.c_str()); }
|
||||||
|
|
||||||
|
FanCall &FanCall::set_preset_mode(const char *preset_mode) {
|
||||||
|
if (preset_mode == nullptr || strlen(preset_mode) == 0) {
|
||||||
|
this->preset_mode_ = nullptr;
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and validate pointer from traits immediately
|
||||||
|
auto traits = this->parent_.get_traits();
|
||||||
|
const char *validated_mode = traits.find_preset_mode(preset_mode);
|
||||||
|
if (validated_mode != nullptr) {
|
||||||
|
this->preset_mode_ = validated_mode; // Store pointer from traits
|
||||||
|
} else {
|
||||||
|
// Preset mode not found in traits - log warning and don't set
|
||||||
|
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), preset_mode);
|
||||||
|
this->preset_mode_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
void FanCall::perform() {
|
void FanCall::perform() {
|
||||||
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
|
ESP_LOGD(TAG, "'%s' - Setting:", this->parent_.get_name().c_str());
|
||||||
this->validate_();
|
this->validate_();
|
||||||
@@ -32,8 +53,8 @@ void FanCall::perform() {
|
|||||||
if (this->direction_.has_value()) {
|
if (this->direction_.has_value()) {
|
||||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
|
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
|
||||||
}
|
}
|
||||||
if (!this->preset_mode_.empty()) {
|
if (this->has_preset_mode()) {
|
||||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_.c_str());
|
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
|
||||||
}
|
}
|
||||||
this->parent_.control(*this);
|
this->parent_.control(*this);
|
||||||
}
|
}
|
||||||
@@ -46,30 +67,15 @@ void FanCall::validate_() {
|
|||||||
|
|
||||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||||
// "Manually setting a speed must disable any set preset mode"
|
// "Manually setting a speed must disable any set preset mode"
|
||||||
this->preset_mode_.clear();
|
this->preset_mode_ = nullptr;
|
||||||
}
|
|
||||||
|
|
||||||
if (!this->preset_mode_.empty()) {
|
|
||||||
const auto &preset_modes = traits.supported_preset_modes();
|
|
||||||
bool found = false;
|
|
||||||
for (const auto &mode : preset_modes) {
|
|
||||||
if (mode == this->preset_mode_) {
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
|
|
||||||
this->preset_mode_.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// when turning on...
|
// when turning on...
|
||||||
if (!this->parent_.state && this->binary_state_.has_value() &&
|
if (!this->parent_.state && this->binary_state_.has_value() &&
|
||||||
*this->binary_state_
|
*this->binary_state_
|
||||||
// ..,and no preset mode will be active...
|
// ..,and no preset mode will be active...
|
||||||
&& this->preset_mode_.empty() &&
|
&& !this->has_preset_mode() &&
|
||||||
this->parent_.preset_mode.empty()
|
this->parent_.get_preset_mode() == nullptr
|
||||||
// ...and neither current nor new speed is available...
|
// ...and neither current nor new speed is available...
|
||||||
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
|
||||||
// ...set speed to 100%
|
// ...set speed to 100%
|
||||||
@@ -99,11 +105,12 @@ FanCall FanRestoreState::to_call(Fan &fan) {
|
|||||||
call.set_speed(this->speed);
|
call.set_speed(this->speed);
|
||||||
call.set_direction(this->direction);
|
call.set_direction(this->direction);
|
||||||
|
|
||||||
if (fan.get_traits().supports_preset_modes()) {
|
auto traits = fan.get_traits();
|
||||||
|
if (traits.supports_preset_modes()) {
|
||||||
// Use stored preset index to get preset name
|
// Use stored preset index to get preset name
|
||||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
const auto &preset_modes = traits.supported_preset_modes();
|
||||||
if (this->preset_mode < preset_modes.size()) {
|
if (this->preset_mode < preset_modes.size()) {
|
||||||
call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode));
|
call.set_preset_mode(preset_modes[this->preset_mode]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return call;
|
return call;
|
||||||
@@ -114,13 +121,12 @@ void FanRestoreState::apply(Fan &fan) {
|
|||||||
fan.speed = this->speed;
|
fan.speed = this->speed;
|
||||||
fan.direction = this->direction;
|
fan.direction = this->direction;
|
||||||
|
|
||||||
if (fan.get_traits().supports_preset_modes()) {
|
// Use stored preset index to get preset name from traits
|
||||||
// Use stored preset index to get preset name
|
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
||||||
const auto &preset_modes = fan.get_traits().supported_preset_modes();
|
if (this->preset_mode < preset_modes.size()) {
|
||||||
if (this->preset_mode < preset_modes.size()) {
|
fan.set_preset_mode_(preset_modes[this->preset_mode]);
|
||||||
fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fan.publish_state();
|
fan.publish_state();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +135,29 @@ FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
|||||||
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
||||||
FanCall Fan::make_call() { return FanCall(*this); }
|
FanCall Fan::make_call() { return FanCall(*this); }
|
||||||
|
|
||||||
|
const char *Fan::find_preset_mode_(const char *preset_mode) { return this->get_traits().find_preset_mode(preset_mode); }
|
||||||
|
|
||||||
|
bool Fan::set_preset_mode_(const char *preset_mode) {
|
||||||
|
if (preset_mode == nullptr) {
|
||||||
|
// Treat nullptr as clearing the preset mode
|
||||||
|
if (this->preset_mode_ == nullptr) {
|
||||||
|
return false; // No change
|
||||||
|
}
|
||||||
|
this->clear_preset_mode_();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const char *validated = this->find_preset_mode_(preset_mode);
|
||||||
|
if (validated == nullptr || this->preset_mode_ == validated) {
|
||||||
|
return false; // Preset mode not supported or no change
|
||||||
|
}
|
||||||
|
this->preset_mode_ = validated;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
|
||||||
|
|
||||||
|
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
|
||||||
|
|
||||||
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||||
void Fan::publish_state() {
|
void Fan::publish_state() {
|
||||||
auto traits = this->get_traits();
|
auto traits = this->get_traits();
|
||||||
@@ -144,8 +173,9 @@ void Fan::publish_state() {
|
|||||||
if (traits.supports_direction()) {
|
if (traits.supports_direction()) {
|
||||||
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
|
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
|
||||||
}
|
}
|
||||||
if (traits.supports_preset_modes() && !this->preset_mode.empty()) {
|
const char *preset = this->get_preset_mode();
|
||||||
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode.c_str());
|
if (traits.supports_preset_modes() && preset != nullptr) {
|
||||||
|
ESP_LOGD(TAG, " Preset Mode: %s", preset);
|
||||||
}
|
}
|
||||||
this->state_callback_.call();
|
this->state_callback_.call();
|
||||||
this->save_state_();
|
this->save_state_();
|
||||||
@@ -189,22 +219,23 @@ void Fan::save_state_() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto traits = this->get_traits();
|
||||||
|
|
||||||
FanRestoreState state{};
|
FanRestoreState state{};
|
||||||
state.state = this->state;
|
state.state = this->state;
|
||||||
state.oscillating = this->oscillating;
|
state.oscillating = this->oscillating;
|
||||||
state.speed = this->speed;
|
state.speed = this->speed;
|
||||||
state.direction = this->direction;
|
state.direction = this->direction;
|
||||||
|
|
||||||
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
|
const char *preset = this->get_preset_mode();
|
||||||
const auto &preset_modes = this->get_traits().supported_preset_modes();
|
if (preset != nullptr) {
|
||||||
// Store index of current preset mode
|
const auto &preset_modes = traits.supported_preset_modes();
|
||||||
size_t i = 0;
|
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
|
||||||
for (const auto &mode : preset_modes) {
|
for (size_t i = 0; i < preset_modes.size(); i++) {
|
||||||
if (mode == this->preset_mode) {
|
if (preset_modes[i] == preset) {
|
||||||
state.preset_mode = i;
|
state.preset_mode = i;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
i++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +259,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
|||||||
}
|
}
|
||||||
if (traits.supports_preset_modes()) {
|
if (traits.supports_preset_modes()) {
|
||||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||||
for (const std::string &s : traits.supported_preset_modes())
|
for (const char *s : traits.supported_preset_modes())
|
||||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s.c_str());
|
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,8 +60,6 @@ class FanCall {
|
|||||||
this->speed_ = speed;
|
this->speed_ = speed;
|
||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
|
|
||||||
FanCall &set_speed(const char *legacy_speed);
|
|
||||||
optional<int> get_speed() const { return this->speed_; }
|
optional<int> get_speed() const { return this->speed_; }
|
||||||
FanCall &set_direction(FanDirection direction) {
|
FanCall &set_direction(FanDirection direction) {
|
||||||
this->direction_ = direction;
|
this->direction_ = direction;
|
||||||
@@ -72,11 +70,10 @@ class FanCall {
|
|||||||
return *this;
|
return *this;
|
||||||
}
|
}
|
||||||
optional<FanDirection> get_direction() const { return this->direction_; }
|
optional<FanDirection> get_direction() const { return this->direction_; }
|
||||||
FanCall &set_preset_mode(const std::string &preset_mode) {
|
FanCall &set_preset_mode(const std::string &preset_mode);
|
||||||
this->preset_mode_ = preset_mode;
|
FanCall &set_preset_mode(const char *preset_mode);
|
||||||
return *this;
|
const char *get_preset_mode() const { return this->preset_mode_; }
|
||||||
}
|
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
|
||||||
std::string get_preset_mode() const { return this->preset_mode_; }
|
|
||||||
|
|
||||||
void perform();
|
void perform();
|
||||||
|
|
||||||
@@ -88,7 +85,7 @@ class FanCall {
|
|||||||
optional<bool> oscillating_;
|
optional<bool> oscillating_;
|
||||||
optional<int> speed_;
|
optional<int> speed_;
|
||||||
optional<FanDirection> direction_{};
|
optional<FanDirection> direction_{};
|
||||||
std::string preset_mode_{};
|
const char *preset_mode_{nullptr}; // Pointer to string in traits (after validation)
|
||||||
};
|
};
|
||||||
|
|
||||||
struct FanRestoreState {
|
struct FanRestoreState {
|
||||||
@@ -114,8 +111,6 @@ class Fan : public EntityBase {
|
|||||||
int speed{0};
|
int speed{0};
|
||||||
/// The current direction of the fan
|
/// The current direction of the fan
|
||||||
FanDirection direction{FanDirection::FORWARD};
|
FanDirection direction{FanDirection::FORWARD};
|
||||||
// The current preset mode of the fan
|
|
||||||
std::string preset_mode{};
|
|
||||||
|
|
||||||
FanCall turn_on();
|
FanCall turn_on();
|
||||||
FanCall turn_off();
|
FanCall turn_off();
|
||||||
@@ -132,8 +127,15 @@ class Fan : public EntityBase {
|
|||||||
/// Set the restore mode of this fan.
|
/// Set the restore mode of this fan.
|
||||||
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||||
|
|
||||||
|
/// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set)
|
||||||
|
const char *get_preset_mode() const { return this->preset_mode_; }
|
||||||
|
|
||||||
|
/// Check if a preset mode is currently active
|
||||||
|
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
friend FanCall;
|
friend FanCall;
|
||||||
|
friend struct FanRestoreState;
|
||||||
|
|
||||||
virtual void control(const FanCall &call) = 0;
|
virtual void control(const FanCall &call) = 0;
|
||||||
|
|
||||||
@@ -142,9 +144,19 @@ class Fan : public EntityBase {
|
|||||||
|
|
||||||
void dump_traits_(const char *tag, const char *prefix);
|
void dump_traits_(const char *tag, const char *prefix);
|
||||||
|
|
||||||
|
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||||
|
bool set_preset_mode_(const char *preset_mode);
|
||||||
|
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
|
||||||
|
bool set_preset_mode_(const std::string &preset_mode);
|
||||||
|
/// Clear the preset mode
|
||||||
|
void clear_preset_mode_();
|
||||||
|
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
|
||||||
|
const char *find_preset_mode_(const char *preset_mode);
|
||||||
|
|
||||||
CallbackManager<void()> state_callback_{};
|
CallbackManager<void()> state_callback_{};
|
||||||
ESPPreferenceObject rtc_;
|
ESPPreferenceObject rtc_;
|
||||||
FanRestoreMode restore_mode_;
|
FanRestoreMode restore_mode_;
|
||||||
|
const char *preset_mode_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fan
|
} // namespace fan
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <initializer_list>
|
||||||
|
|
||||||
namespace esphome {
|
namespace esphome {
|
||||||
|
|
||||||
#ifdef USE_API
|
|
||||||
namespace api {
|
|
||||||
class APIConnection;
|
|
||||||
} // namespace api
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace fan {
|
namespace fan {
|
||||||
|
|
||||||
class FanTraits {
|
class FanTraits {
|
||||||
@@ -35,27 +31,38 @@ class FanTraits {
|
|||||||
/// Set whether this fan supports changing direction
|
/// Set whether this fan supports changing direction
|
||||||
void set_direction(bool direction) { this->direction_ = direction; }
|
void set_direction(bool direction) { this->direction_ = direction; }
|
||||||
/// Return the preset modes supported by the fan.
|
/// Return the preset modes supported by the fan.
|
||||||
const std::vector<std::string> &supported_preset_modes() const { return this->preset_modes_; }
|
const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; }
|
||||||
/// Set the preset modes supported by the fan.
|
/// Set the preset modes supported by the fan (from initializer list).
|
||||||
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
|
void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) {
|
||||||
|
this->preset_modes_ = preset_modes;
|
||||||
|
}
|
||||||
|
/// Set the preset modes supported by the fan (from vector).
|
||||||
|
void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; }
|
||||||
|
|
||||||
|
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
|
||||||
|
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) = delete;
|
||||||
|
void set_supported_preset_modes(std::initializer_list<std::string> preset_modes) = delete;
|
||||||
|
|
||||||
/// Return if preset modes are supported
|
/// Return if preset modes are supported
|
||||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||||
|
/// Find and return the matching preset mode pointer from supported modes, or nullptr if not found.
|
||||||
|
const char *find_preset_mode(const char *preset_mode) const {
|
||||||
|
if (preset_mode == nullptr)
|
||||||
|
return nullptr;
|
||||||
|
for (const char *mode : this->preset_modes_) {
|
||||||
|
if (strcmp(mode, preset_mode) == 0) {
|
||||||
|
return mode; // Return pointer from traits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
#ifdef USE_API
|
|
||||||
// The API connection is a friend class to access internal methods
|
|
||||||
friend class api::APIConnection;
|
|
||||||
// This method returns a reference to the internal preset modes.
|
|
||||||
// It is used by the API to avoid copying data when encoding messages.
|
|
||||||
// Warning: Do not use this method outside of the API connection code.
|
|
||||||
// It returns a reference to internal data that can be invalidated.
|
|
||||||
const std::vector<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
|
|
||||||
#endif
|
|
||||||
bool oscillation_{false};
|
bool oscillation_{false};
|
||||||
bool speed_{false};
|
bool speed_{false};
|
||||||
bool direction_{false};
|
bool direction_{false};
|
||||||
int speed_count_{};
|
int speed_count_{};
|
||||||
std::vector<std::string> preset_modes_{};
|
std::vector<const char *> preset_modes_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fan
|
} // namespace fan
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ CONFIG_SCHEMA = (
|
|||||||
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
# due to hardware limitations or lack of reliable interrupt support. This ensures
|
||||||
# stable operation on these platforms. Future maintainers should verify platform
|
# stable operation on these platforms. Future maintainers should verify platform
|
||||||
# capabilities before changing this default behavior.
|
# capabilities before changing this default behavior.
|
||||||
|
# nrf52 has no gpio interrupts implemented yet
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_USE_INTERRUPT,
|
CONF_USE_INTERRUPT,
|
||||||
bk72xx=False,
|
bk72xx=False,
|
||||||
@@ -46,7 +47,7 @@ CONFIG_SCHEMA = (
|
|||||||
esp8266=True,
|
esp8266=True,
|
||||||
host=True,
|
host=True,
|
||||||
ln882x=False,
|
ln882x=False,
|
||||||
nrf52=True,
|
nrf52=False,
|
||||||
rp2040=True,
|
rp2040=True,
|
||||||
rtl87xx=False,
|
rtl87xx=False,
|
||||||
): cv.boolean,
|
): cv.boolean,
|
||||||
@@ -94,6 +95,8 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
use_interrupt = False
|
use_interrupt = False
|
||||||
|
|
||||||
cg.add(var.set_use_interrupt(use_interrupt))
|
|
||||||
if use_interrupt:
|
if use_interrupt:
|
||||||
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
|
||||||
|
else:
|
||||||
|
# Only generate call when disabling interrupts (default is true)
|
||||||
|
cg.add(var.set_use_interrupt(use_interrupt))
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
|
|||||||
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
|
PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
|
||||||
}
|
}
|
||||||
|
|
||||||
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
|
void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) {
|
||||||
this->traits_.set_supported_swing_modes(modes);
|
this->traits_.set_supported_swing_modes(modes);
|
||||||
if (!modes.empty())
|
if (!modes.empty())
|
||||||
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
|
this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
|
||||||
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
|
|||||||
|
|
||||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
|
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
|
||||||
|
|
||||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
|
void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) {
|
||||||
this->traits_.set_supported_modes(modes);
|
this->traits_.set_supported_modes(modes);
|
||||||
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF); // Always available
|
||||||
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
|
this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); // Always available
|
||||||
}
|
}
|
||||||
|
|
||||||
void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) {
|
void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) {
|
||||||
this->traits_.set_supported_presets(presets);
|
this->traits_.set_supported_presets(presets);
|
||||||
if (!presets.empty())
|
if (!presets.empty())
|
||||||
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
|
this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <set>
|
|
||||||
#include "esphome/components/climate/climate.h"
|
#include "esphome/components/climate/climate.h"
|
||||||
#include "esphome/components/uart/uart.h"
|
#include "esphome/components/uart/uart.h"
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
@@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component,
|
|||||||
void send_power_off_command();
|
void send_power_off_command();
|
||||||
void toggle_power();
|
void toggle_power();
|
||||||
void reset_protocol() { this->reset_protocol_request_ = true; };
|
void reset_protocol() { this->reset_protocol_request_ = true; };
|
||||||
void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
|
void set_supported_modes(esphome::climate::ClimateModeMask modes);
|
||||||
void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
|
void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes);
|
||||||
void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
|
void set_supported_presets(esphome::climate::ClimatePresetMask presets);
|
||||||
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
|
bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
|
||||||
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
|
size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
|
||||||
size_t read_array(uint8_t *data, size_t len) noexcept override {
|
size_t read_array(uint8_t *data, size_t len) noexcept override {
|
||||||
|
|||||||
@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
|||||||
{
|
{
|
||||||
// Swing mode
|
// Swing mode
|
||||||
ClimateSwingMode old_swing_mode = this->swing_mode;
|
ClimateSwingMode old_swing_mode = this->swing_mode;
|
||||||
const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
|
const auto &swing_modes = traits_.get_supported_swing_modes();
|
||||||
bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
|
bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL);
|
||||||
bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
|
bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL);
|
||||||
if (horizontal_swing_supported &&
|
if (horizontal_swing_supported &&
|
||||||
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
|
(packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
|
||||||
if (vertical_swing_supported &&
|
if (vertical_swing_supported &&
|
||||||
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
|
|||||||
(uint8_t) hon_protocol::DataParameters::QUIET_MODE,
|
(uint8_t) hon_protocol::DataParameters::QUIET_MODE,
|
||||||
quiet_mode_buf, 2);
|
quiet_mode_buf, 2);
|
||||||
}
|
}
|
||||||
if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
|
if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) {
|
||||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||||
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
|
(uint8_t) hon_protocol::DataParameters::FAST_MODE,
|
||||||
fast_mode_buf, 2);
|
fast_mode_buf, 2);
|
||||||
}
|
}
|
||||||
if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
|
if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) {
|
||||||
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
|
||||||
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
(uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
|
||||||
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
|
(uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) {
|
|||||||
this->oscillating = *call.get_oscillating();
|
this->oscillating = *call.get_oscillating();
|
||||||
if (call.get_direction().has_value())
|
if (call.get_direction().has_value())
|
||||||
this->direction = *call.get_direction();
|
this->direction = *call.get_direction();
|
||||||
this->preset_mode = call.get_preset_mode();
|
this->set_preset_mode_(call.get_preset_mode());
|
||||||
|
|
||||||
this->write_state_();
|
this->write_state_();
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
#include "esphome/core/automation.h"
|
#include "esphome/core/automation.h"
|
||||||
#include "esphome/components/output/binary_output.h"
|
#include "esphome/components/output/binary_output.h"
|
||||||
#include "esphome/components/output/float_output.h"
|
#include "esphome/components/output/float_output.h"
|
||||||
@@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
|||||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||||
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
|
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
|
||||||
void set_preset_modes(const std::vector<std::string> &presets) { preset_modes_ = presets; }
|
void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; }
|
||||||
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void dump_config() override;
|
void dump_config() override;
|
||||||
@@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
|||||||
int speed_count_{};
|
int speed_count_{};
|
||||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||||
fan::FanTraits traits_;
|
fan::FanTraits traits_;
|
||||||
std::vector<std::string> preset_modes_{};
|
std::vector<const char *> preset_modes_{};
|
||||||
|
|
||||||
void control(const fan::FanCall &call) override;
|
void control(const fan::FanCall &call) override;
|
||||||
void write_state_();
|
void write_state_();
|
||||||
|
|||||||
1
esphome/components/hdc2010/__init__.py
Normal file
1
esphome/components/hdc2010/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
CODEOWNERS = ["@optimusprimespace", "@ssieb"]
|
||||||
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
111
esphome/components/hdc2010/hdc2010.cpp
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
#include "esphome/core/hal.h"
|
||||||
|
#include "hdc2010.h"
|
||||||
|
// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js
|
||||||
|
// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp
|
||||||
|
namespace esphome {
|
||||||
|
namespace hdc2010 {
|
||||||
|
|
||||||
|
static const char *const TAG = "hdc2010";
|
||||||
|
|
||||||
|
static const uint8_t HDC2010_ADDRESS = 0x40; // 0b1000000 or 0b1000001 from datasheet
|
||||||
|
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
|
||||||
|
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
|
||||||
|
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
|
||||||
|
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
|
||||||
|
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
|
||||||
|
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
|
||||||
|
static const uint8_t CONFIG = 0x0E;
|
||||||
|
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
|
||||||
|
|
||||||
|
void HDC2010Component::setup() {
|
||||||
|
ESP_LOGCONFIG(TAG, "Running setup");
|
||||||
|
|
||||||
|
const uint8_t data[2] = {
|
||||||
|
0b00000000, // resolution 14bit for both humidity and temperature
|
||||||
|
0b00000000 // reserved
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
|
||||||
|
ESP_LOGW(TAG, "Initial config instruction error");
|
||||||
|
this->status_set_warning();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set measurement mode to temperature and humidity
|
||||||
|
uint8_t config_contents;
|
||||||
|
this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||||
|
config_contents = (config_contents & 0xF9); // Always set to TEMP_AND_HUMID mode
|
||||||
|
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||||
|
|
||||||
|
// Set rate to manual
|
||||||
|
this->read_register(CONFIG, &config_contents, 1);
|
||||||
|
config_contents &= 0x8F;
|
||||||
|
this->write_bytes(CONFIG, &config_contents, 1);
|
||||||
|
|
||||||
|
// Set temperature resolution to 14bit
|
||||||
|
this->read_register(CONFIG, &config_contents, 1);
|
||||||
|
config_contents &= 0x3F;
|
||||||
|
this->write_bytes(CONFIG, &config_contents, 1);
|
||||||
|
|
||||||
|
// Set humidity resolution to 14bit
|
||||||
|
this->read_register(CONFIG, &config_contents, 1);
|
||||||
|
config_contents &= 0xCF;
|
||||||
|
this->write_bytes(CONFIG, &config_contents, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HDC2010Component::dump_config() {
|
||||||
|
ESP_LOGCONFIG(TAG, "HDC2010:");
|
||||||
|
LOG_I2C_DEVICE(this);
|
||||||
|
if (this->is_failed()) {
|
||||||
|
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||||
|
}
|
||||||
|
LOG_UPDATE_INTERVAL(this);
|
||||||
|
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||||
|
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||||
|
}
|
||||||
|
|
||||||
|
void HDC2010Component::update() {
|
||||||
|
// Trigger measurement
|
||||||
|
uint8_t config_contents;
|
||||||
|
this->read_register(CONFIG, &config_contents, 1);
|
||||||
|
config_contents |= 0x01;
|
||||||
|
this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
|
||||||
|
|
||||||
|
// 1ms delay after triggering the sample
|
||||||
|
set_timeout(1, [this]() {
|
||||||
|
if (this->temperature_sensor_ != nullptr) {
|
||||||
|
float temp = this->read_temp();
|
||||||
|
this->temperature_sensor_->publish_state(temp);
|
||||||
|
ESP_LOGD(TAG, "Temp=%.1f°C", temp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->humidity_sensor_ != nullptr) {
|
||||||
|
float humidity = this->read_humidity();
|
||||||
|
this->humidity_sensor_->publish_state(humidity);
|
||||||
|
ESP_LOGD(TAG, "Humidity=%.1f%%", humidity);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
float HDC2010Component::read_temp() {
|
||||||
|
uint8_t byte[2];
|
||||||
|
|
||||||
|
this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
|
||||||
|
this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
|
||||||
|
|
||||||
|
uint16_t temp = encode_uint16(byte[1], byte[0]);
|
||||||
|
return (float) temp * 0.0025177f - 40.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float HDC2010Component::read_humidity() {
|
||||||
|
uint8_t byte[2];
|
||||||
|
|
||||||
|
this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
|
||||||
|
this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
|
||||||
|
|
||||||
|
uint16_t humidity = encode_uint16(byte[1], byte[0]);
|
||||||
|
return (float) humidity * 0.001525879f;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace hdc2010
|
||||||
|
} // namespace esphome
|
||||||
32
esphome/components/hdc2010/hdc2010.h
Normal file
32
esphome/components/hdc2010/hdc2010.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/components/sensor/sensor.h"
|
||||||
|
#include "esphome/components/i2c/i2c.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace hdc2010 {
|
||||||
|
|
||||||
|
class HDC2010Component : public PollingComponent, public i2c::I2CDevice {
|
||||||
|
public:
|
||||||
|
void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
|
||||||
|
|
||||||
|
void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
|
||||||
|
|
||||||
|
/// Setup the sensor and check for connection.
|
||||||
|
void setup() override;
|
||||||
|
void dump_config() override;
|
||||||
|
/// Retrieve the latest sensor values. This operation takes approximately 16ms.
|
||||||
|
void update() override;
|
||||||
|
|
||||||
|
float read_temp();
|
||||||
|
|
||||||
|
float read_humidity();
|
||||||
|
|
||||||
|
protected:
|
||||||
|
sensor::Sensor *temperature_sensor_{nullptr};
|
||||||
|
sensor::Sensor *humidity_sensor_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace hdc2010
|
||||||
|
} // namespace esphome
|
||||||
56
esphome/components/hdc2010/sensor.py
Normal file
56
esphome/components/hdc2010/sensor.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from esphome.components import i2c, sensor
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_HUMIDITY,
|
||||||
|
CONF_ID,
|
||||||
|
CONF_TEMPERATURE,
|
||||||
|
DEVICE_CLASS_HUMIDITY,
|
||||||
|
DEVICE_CLASS_TEMPERATURE,
|
||||||
|
STATE_CLASS_MEASUREMENT,
|
||||||
|
UNIT_CELSIUS,
|
||||||
|
UNIT_PERCENT,
|
||||||
|
)
|
||||||
|
|
||||||
|
DEPENDENCIES = ["i2c"]
|
||||||
|
|
||||||
|
hdc2010_ns = cg.esphome_ns.namespace("hdc2010")
|
||||||
|
HDC2010Component = hdc2010_ns.class_(
|
||||||
|
"HDC2010Component", cg.PollingComponent, i2c.I2CDevice
|
||||||
|
)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = (
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(HDC2010Component),
|
||||||
|
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_CELSIUS,
|
||||||
|
accuracy_decimals=1,
|
||||||
|
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||||
|
unit_of_measurement=UNIT_PERCENT,
|
||||||
|
accuracy_decimals=0,
|
||||||
|
device_class=DEVICE_CLASS_HUMIDITY,
|
||||||
|
state_class=STATE_CLASS_MEASUREMENT,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.extend(cv.polling_component_schema("60s"))
|
||||||
|
.extend(i2c.i2c_device_schema(0x40))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
await i2c.register_i2c_device(var, config)
|
||||||
|
|
||||||
|
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||||
|
sens = await sensor.new_sensor(temperature_config)
|
||||||
|
cg.add(var.set_temperature_sensor(sens))
|
||||||
|
|
||||||
|
if humidity_config := config.get(CONF_HUMIDITY):
|
||||||
|
sens = await sensor.new_sensor(humidity_config)
|
||||||
|
cg.add(var.set_humidity_sensor(sens))
|
||||||
@@ -97,12 +97,11 @@ const float TEMP_MAX = 100; // Celsius
|
|||||||
class HeatpumpIRClimate : public climate_ir::ClimateIR {
|
class HeatpumpIRClimate : public climate_ir::ClimateIR {
|
||||||
public:
|
public:
|
||||||
HeatpumpIRClimate()
|
HeatpumpIRClimate()
|
||||||
: climate_ir::ClimateIR(
|
: climate_ir::ClimateIR(TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
||||||
TEMP_MIN, TEMP_MAX, 1.0f, true, true,
|
{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH,
|
||||||
std::set<climate::ClimateFanMode>{climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM,
|
climate::CLIMATE_FAN_AUTO},
|
||||||
climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_AUTO},
|
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
||||||
std::set<climate::ClimateSwingMode>{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_HORIZONTAL,
|
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
||||||
climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_BOTH}) {}
|
|
||||||
void setup() override;
|
void setup() override;
|
||||||
void set_protocol(Protocol protocol) { this->protocol_ = protocol; }
|
void set_protocol(Protocol protocol) { this->protocol_ = protocol; }
|
||||||
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
void set_horizontal_default(HorizontalDirection horizontal_direction) {
|
||||||
|
|||||||
@@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin {
|
|||||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||||
|
|
||||||
uint8_t pin_;
|
uint8_t pin_;
|
||||||
bool inverted_;
|
bool inverted_{};
|
||||||
gpio::Flags flags_;
|
gpio::Flags flags_{};
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace host
|
} // namespace host
|
||||||
|
|||||||
@@ -57,6 +57,9 @@ async def host_pin_to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
num = config[CONF_NUMBER]
|
num = config[CONF_NUMBER]
|
||||||
cg.add(var.set_pin(num))
|
cg.add(var.set_pin(num))
|
||||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
# Only set if true to avoid bloating setup() function
|
||||||
|
# (inverted bit in pin_flags_ bitfield is zero-initialized to false)
|
||||||
|
if config[CONF_INVERTED]:
|
||||||
|
cg.add(var.set_inverted(True))
|
||||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||||
return var
|
return var
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ from esphome.const import (
|
|||||||
CONF_ON_ERROR,
|
CONF_ON_ERROR,
|
||||||
CONF_ON_RESPONSE,
|
CONF_ON_RESPONSE,
|
||||||
CONF_TIMEOUT,
|
CONF_TIMEOUT,
|
||||||
CONF_TRIGGER_ID,
|
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
CONF_WATCHDOG_TIMEOUT,
|
CONF_WATCHDOG_TIMEOUT,
|
||||||
PLATFORM_HOST,
|
PLATFORM_HOST,
|
||||||
@@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
|
|||||||
f"{CONF_VERIFY_SSL} has moved to the base component configuration."
|
f"{CONF_VERIFY_SSL} has moved to the base component configuration."
|
||||||
),
|
),
|
||||||
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
||||||
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
|
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True),
|
||||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
|
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
|
||||||
),
|
|
||||||
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
|
|
||||||
{
|
|
||||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
|
||||||
automation.Trigger.template()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
|
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
|
|||||||
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
|
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
|
||||||
cg.add(var.set_url(template_))
|
cg.add(var.set_url(template_))
|
||||||
cg.add(var.set_method(config[CONF_METHOD]))
|
cg.add(var.set_method(config[CONF_METHOD]))
|
||||||
cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
|
|
||||||
|
capture_response = config[CONF_CAPTURE_RESPONSE]
|
||||||
|
if capture_response:
|
||||||
|
cg.add(var.set_capture_response(capture_response))
|
||||||
|
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
|
||||||
|
|
||||||
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
|
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
|
||||||
|
|
||||||
if CONF_BODY in config:
|
if CONF_BODY in config:
|
||||||
@@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
|
|||||||
for value in config.get(CONF_COLLECT_HEADERS, []):
|
for value in config.get(CONF_COLLECT_HEADERS, []):
|
||||||
cg.add(var.add_collect_header(value))
|
cg.add(var.add_collect_header(value))
|
||||||
|
|
||||||
for conf in config.get(CONF_ON_RESPONSE, []):
|
if response_conf := config.get(CONF_ON_RESPONSE):
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
if capture_response:
|
||||||
cg.add(var.register_response_trigger(trigger))
|
await automation.build_automation(
|
||||||
await automation.build_automation(
|
var.get_success_trigger_with_response(),
|
||||||
trigger,
|
[
|
||||||
[
|
(cg.std_shared_ptr.template(HttpContainer), "response"),
|
||||||
(cg.std_shared_ptr.template(HttpContainer), "response"),
|
(cg.std_string_ref, "body"),
|
||||||
(cg.std_string_ref, "body"),
|
*args,
|
||||||
],
|
],
|
||||||
conf,
|
response_conf,
|
||||||
)
|
)
|
||||||
for conf in config.get(CONF_ON_ERROR, []):
|
else:
|
||||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
|
await automation.build_automation(
|
||||||
cg.add(var.register_error_trigger(trigger))
|
var.get_success_trigger(),
|
||||||
await automation.build_automation(trigger, [], conf)
|
[(cg.std_shared_ptr.template(HttpContainer), "response"), *args],
|
||||||
|
response_conf,
|
||||||
|
)
|
||||||
|
|
||||||
|
if error_conf := config.get(CONF_ON_ERROR):
|
||||||
|
await automation.build_automation(var.get_error_trigger(), args, error_conf)
|
||||||
|
|
||||||
return var
|
return var
|
||||||
|
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ class HttpRequestComponent : public Component {
|
|||||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||||
|
|
||||||
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
|
||||||
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
|
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
|
||||||
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
|
||||||
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
|
||||||
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
|
||||||
@@ -169,11 +169,11 @@ class HttpRequestComponent : public Component {
|
|||||||
protected:
|
protected:
|
||||||
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
|
virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method,
|
||||||
const std::string &body, const std::list<Header> &request_headers,
|
const std::string &body, const std::list<Header> &request_headers,
|
||||||
std::set<std::string> collect_headers) = 0;
|
const std::set<std::string> &collect_headers) = 0;
|
||||||
const char *useragent_{nullptr};
|
const char *useragent_{nullptr};
|
||||||
bool follow_redirects_{};
|
bool follow_redirects_{};
|
||||||
uint16_t redirect_limit_{};
|
uint16_t redirect_limit_{};
|
||||||
uint16_t timeout_{4500};
|
uint32_t timeout_{4500};
|
||||||
uint32_t watchdog_timeout_{0};
|
uint32_t watchdog_timeout_{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -183,7 +183,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
TEMPLATABLE_VALUE(std::string, url)
|
TEMPLATABLE_VALUE(std::string, url)
|
||||||
TEMPLATABLE_VALUE(const char *, method)
|
TEMPLATABLE_VALUE(const char *, method)
|
||||||
TEMPLATABLE_VALUE(std::string, body)
|
TEMPLATABLE_VALUE(std::string, body)
|
||||||
|
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||||
TEMPLATABLE_VALUE(bool, capture_response)
|
TEMPLATABLE_VALUE(bool, capture_response)
|
||||||
|
#endif
|
||||||
|
|
||||||
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
|
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
|
||||||
this->request_headers_.insert({key, value});
|
this->request_headers_.insert({key, value});
|
||||||
@@ -195,9 +197,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
|
|
||||||
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
|
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
|
||||||
|
|
||||||
void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
|
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||||
|
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const {
|
||||||
|
return this->success_trigger_with_response_;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||||
|
|
||||||
void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); }
|
Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||||
|
|
||||||
void set_max_response_buffer_size(size_t max_response_buffer_size) {
|
void set_max_response_buffer_size(size_t max_response_buffer_size) {
|
||||||
this->max_response_buffer_size_ = max_response_buffer_size;
|
this->max_response_buffer_size_ = max_response_buffer_size;
|
||||||
@@ -228,17 +235,20 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
|
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
|
||||||
this->collect_headers_);
|
this->collect_headers_);
|
||||||
|
|
||||||
|
auto captured_args = std::make_tuple(x...);
|
||||||
|
|
||||||
if (container == nullptr) {
|
if (container == nullptr) {
|
||||||
for (auto *trigger : this->error_triggers_)
|
std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); },
|
||||||
trigger->trigger();
|
captured_args);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t content_length = container->content_length;
|
size_t content_length = container->content_length;
|
||||||
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
|
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
|
||||||
|
|
||||||
std::string response_body;
|
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||||
if (this->capture_response_.value(x...)) {
|
if (this->capture_response_.value(x...)) {
|
||||||
|
std::string response_body;
|
||||||
RAMAllocator<uint8_t> allocator;
|
RAMAllocator<uint8_t> allocator;
|
||||||
uint8_t *buf = allocator.allocate(max_length);
|
uint8_t *buf = allocator.allocate(max_length);
|
||||||
if (buf != nullptr) {
|
if (buf != nullptr) {
|
||||||
@@ -253,19 +263,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
response_body.assign((char *) buf, read_index);
|
response_body.assign((char *) buf, read_index);
|
||||||
allocator.deallocate(buf, max_length);
|
allocator.deallocate(buf, max_length);
|
||||||
}
|
}
|
||||||
}
|
std::apply(
|
||||||
|
[this, &container, &response_body](Ts... captured_args_inner) {
|
||||||
if (this->response_triggers_.size() == 1) {
|
this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...);
|
||||||
// if there is only one trigger, no need to copy the response body
|
},
|
||||||
this->response_triggers_[0]->process(container, response_body);
|
captured_args);
|
||||||
} else {
|
} else
|
||||||
for (auto *trigger : this->response_triggers_) {
|
#endif
|
||||||
// with multiple triggers, pass a copy of the response body to each
|
{
|
||||||
// one so that modifications made in one trigger are not visible to
|
std::apply([this, &container](
|
||||||
// the others
|
Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); },
|
||||||
auto response_body_copy = std::string(response_body);
|
captured_args);
|
||||||
trigger->process(container, response_body_copy);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
container->end();
|
container->end();
|
||||||
}
|
}
|
||||||
@@ -283,8 +291,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
|
|||||||
std::set<std::string> collect_headers_{"content-type", "content-length"};
|
std::set<std::string> collect_headers_{"content-type", "content-length"};
|
||||||
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
|
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
|
||||||
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
|
||||||
std::vector<HttpRequestResponseTrigger *> response_triggers_{};
|
#ifdef USE_HTTP_REQUEST_RESPONSE
|
||||||
std::vector<Trigger<> *> error_triggers_{};
|
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ =
|
||||||
|
new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>();
|
||||||
|
#endif
|
||||||
|
Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ =
|
||||||
|
new Trigger<std::shared_ptr<HttpContainer>, Ts...>();
|
||||||
|
Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>();
|
||||||
|
|
||||||
size_t max_response_buffer_size_{SIZE_MAX};
|
size_t max_response_buffer_size_{SIZE_MAX};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino";
|
|||||||
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
|
std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method,
|
||||||
const std::string &body,
|
const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> collect_headers) {
|
const std::set<std::string> &collect_headers) {
|
||||||
if (!network::is_connected()) {
|
if (!network::is_connected()) {
|
||||||
this->status_momentary_error("failed", 1000);
|
this->status_momentary_error("failed", 1000);
|
||||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent {
|
|||||||
protected:
|
protected:
|
||||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> collect_headers) override;
|
const std::set<std::string> &collect_headers) override;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace http_request
|
} // namespace http_request
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host";
|
|||||||
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
|
std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method,
|
||||||
const std::string &body,
|
const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> response_headers) {
|
const std::set<std::string> &response_headers) {
|
||||||
if (!network::is_connected()) {
|
if (!network::is_connected()) {
|
||||||
this->status_momentary_error("failed", 1000);
|
this->status_momentary_error("failed", 1000);
|
||||||
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
ESP_LOGW(TAG, "HTTP Request failed; Not connected to network");
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent {
|
|||||||
public:
|
public:
|
||||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> response_headers) override;
|
const std::set<std::string> &response_headers) override;
|
||||||
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
|
void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
|
|||||||
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
|
std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method,
|
||||||
const std::string &body,
|
const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> collect_headers) {
|
const std::set<std::string> &collect_headers) {
|
||||||
if (!network::is_connected()) {
|
if (!network::is_connected()) {
|
||||||
this->status_momentary_error("failed", 1000);
|
this->status_momentary_error("failed", 1000);
|
||||||
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
ESP_LOGE(TAG, "HTTP Request failed; Not connected to network");
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent {
|
|||||||
protected:
|
protected:
|
||||||
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body,
|
||||||
const std::list<Header> &request_headers,
|
const std::list<Header> &request_headers,
|
||||||
std::set<std::string> collect_headers) override;
|
const std::set<std::string> &collect_headers) override;
|
||||||
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
// if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE
|
||||||
uint16_t buffer_size_rx_{};
|
uint16_t buffer_size_rx_{};
|
||||||
uint16_t buffer_size_tx_{};
|
uint16_t buffer_size_tx_{};
|
||||||
|
|||||||
@@ -671,18 +671,33 @@ async def write_image(config, all_frames=False):
|
|||||||
resize = config.get(CONF_RESIZE)
|
resize = config.get(CONF_RESIZE)
|
||||||
if is_svg_file(path):
|
if is_svg_file(path):
|
||||||
# Local import so use of non-SVG files needn't require cairosvg installed
|
# Local import so use of non-SVG files needn't require cairosvg installed
|
||||||
|
from pyexpat import ExpatError
|
||||||
|
from xml.etree.ElementTree import ParseError
|
||||||
|
|
||||||
from cairosvg import svg2png
|
from cairosvg import svg2png
|
||||||
|
from cairosvg.helpers import PointError
|
||||||
|
|
||||||
if not resize:
|
if not resize:
|
||||||
resize = (None, None)
|
resize = (None, None)
|
||||||
with open(path, "rb") as file:
|
try:
|
||||||
image = svg2png(
|
with open(path, "rb") as file:
|
||||||
file_obj=file,
|
image = svg2png(
|
||||||
output_width=resize[0],
|
file_obj=file,
|
||||||
output_height=resize[1],
|
output_width=resize[0],
|
||||||
)
|
output_height=resize[1],
|
||||||
image = Image.open(io.BytesIO(image))
|
)
|
||||||
width, height = image.size
|
image = Image.open(io.BytesIO(image))
|
||||||
|
width, height = image.size
|
||||||
|
except (
|
||||||
|
ValueError,
|
||||||
|
ParseError,
|
||||||
|
IndexError,
|
||||||
|
ExpatError,
|
||||||
|
AttributeError,
|
||||||
|
TypeError,
|
||||||
|
PointError,
|
||||||
|
) as e:
|
||||||
|
raise core.EsphomeError(f"Could not load SVG image {path}: {e}") from e
|
||||||
else:
|
else:
|
||||||
image = Image.open(path)
|
image = Image.open(path)
|
||||||
width, height = image.size
|
width, height = image.size
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
|||||||
|
|
||||||
case IMAGE_TYPE_RGB:
|
case IMAGE_TYPE_RGB:
|
||||||
#if LV_COLOR_DEPTH == 32
|
#if LV_COLOR_DEPTH == 32
|
||||||
switch (this->transparent_) {
|
switch (this->transparency_) {
|
||||||
case TRANSPARENCY_ALPHA_CHANNEL:
|
case TRANSPARENCY_ALPHA_CHANNEL:
|
||||||
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
|
||||||
break;
|
break;
|
||||||
@@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
this->dsc_.header.cf =
|
||||||
|
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,38 @@ void ImprovSerialComponent::setup() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ImprovSerialComponent::loop() {
|
||||||
|
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
||||||
|
this->last_read_byte_ = 0;
|
||||||
|
this->rx_buffer_.clear();
|
||||||
|
ESP_LOGV(TAG, "Timeout");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto byte = this->read_byte_();
|
||||||
|
while (byte.has_value()) {
|
||||||
|
if (this->parse_improv_serial_byte_(byte.value())) {
|
||||||
|
this->last_read_byte_ = millis();
|
||||||
|
} else {
|
||||||
|
this->last_read_byte_ = 0;
|
||||||
|
this->rx_buffer_.clear();
|
||||||
|
}
|
||||||
|
byte = this->read_byte_();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||||
|
if (wifi::global_wifi_component->is_connected()) {
|
||||||
|
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||||
|
this->connecting_sta_.get_password());
|
||||||
|
this->connecting_sta_ = {};
|
||||||
|
this->cancel_timeout("wifi-connect-timeout");
|
||||||
|
this->set_state_(improv::STATE_PROVISIONED);
|
||||||
|
|
||||||
|
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
|
||||||
|
this->send_response_(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
||||||
|
|
||||||
optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
||||||
@@ -78,8 +110,28 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() {
|
|||||||
return byte;
|
return byte;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
|
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
|
||||||
data.push_back('\n');
|
// First, set length field
|
||||||
|
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
|
||||||
|
|
||||||
|
const bool there_is_data = data != nullptr && size > 0;
|
||||||
|
// If there_is_data, checksum must not include our optional data byte
|
||||||
|
const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2;
|
||||||
|
// Only transmit the full buffer length if there is no data (only state/error byte is provided in this case)
|
||||||
|
const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE;
|
||||||
|
// Calculate checksum for message
|
||||||
|
uint8_t checksum = 0;
|
||||||
|
for (uint8_t i = 0; i < header_checksum_len; i++) {
|
||||||
|
checksum += this->tx_header_[i];
|
||||||
|
}
|
||||||
|
if (there_is_data) {
|
||||||
|
// Include data in checksum
|
||||||
|
for (size_t i = 0; i < size; i++) {
|
||||||
|
checksum += data[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
switch (logger::global_logger->get_uart()) {
|
switch (logger::global_logger->get_uart()) {
|
||||||
case logger::UART_SELECTION_UART0:
|
case logger::UART_SELECTION_UART0:
|
||||||
@@ -87,63 +139,45 @@ void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) {
|
|||||||
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||||
!defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
!defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
|
||||||
case logger::UART_SELECTION_UART2:
|
case logger::UART_SELECTION_UART2:
|
||||||
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
|
#endif
|
||||||
uart_write_bytes(this->uart_num_, data.data(), data.size());
|
uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len);
|
||||||
|
if (there_is_data) {
|
||||||
|
uart_write_bytes(this->uart_num_, data, size);
|
||||||
|
uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
|
||||||
case logger::UART_SELECTION_USB_CDC: {
|
case logger::UART_SELECTION_USB_CDC:
|
||||||
const char *msg = (char *) data.data();
|
esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len);
|
||||||
esp_usb_console_write_buf(msg, data.size());
|
if (there_is_data) {
|
||||||
|
esp_usb_console_write_buf((const char *) data, size);
|
||||||
|
esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX],
|
||||||
|
2); // Footer: checksum and newline
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
#endif
|
||||||
#endif // USE_LOGGER_USB_CDC
|
|
||||||
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
#ifdef USE_LOGGER_USB_SERIAL_JTAG
|
||||||
case logger::UART_SELECTION_USB_SERIAL_JTAG:
|
case logger::UART_SELECTION_USB_SERIAL_JTAG:
|
||||||
usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS);
|
usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS);
|
||||||
delay(10);
|
if (there_is_data) {
|
||||||
usb_serial_jtag_ll_txfifo_flush(); // fixes for issue in IDF 4.4.7
|
usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS);
|
||||||
|
usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2,
|
||||||
|
20 / portTICK_PERIOD_MS); // Footer: checksum and newline
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
#endif // USE_LOGGER_USB_SERIAL_JTAG
|
#endif
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
#elif defined(USE_ARDUINO)
|
#elif defined(USE_ARDUINO)
|
||||||
this->hw_serial_->write(data.data(), data.size());
|
this->hw_serial_->write(this->tx_header_, header_tx_len);
|
||||||
|
if (there_is_data) {
|
||||||
|
this->hw_serial_->write(data, size);
|
||||||
|
this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2); // Footer: checksum and newline
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::loop() {
|
|
||||||
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
|
|
||||||
this->last_read_byte_ = 0;
|
|
||||||
this->rx_buffer_.clear();
|
|
||||||
ESP_LOGV(TAG, "Improv Serial timeout");
|
|
||||||
}
|
|
||||||
|
|
||||||
auto byte = this->read_byte_();
|
|
||||||
while (byte.has_value()) {
|
|
||||||
if (this->parse_improv_serial_byte_(byte.value())) {
|
|
||||||
this->last_read_byte_ = millis();
|
|
||||||
} else {
|
|
||||||
this->last_read_byte_ = 0;
|
|
||||||
this->rx_buffer_.clear();
|
|
||||||
}
|
|
||||||
byte = this->read_byte_();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
|
||||||
if (wifi::global_wifi_component->is_connected()) {
|
|
||||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
|
||||||
this->connecting_sta_.get_password());
|
|
||||||
this->connecting_sta_ = {};
|
|
||||||
this->cancel_timeout("wifi-connect-timeout");
|
|
||||||
this->set_state_(improv::STATE_PROVISIONED);
|
|
||||||
|
|
||||||
std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS);
|
|
||||||
this->send_response_(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) {
|
||||||
std::vector<std::string> urls;
|
std::vector<std::string> urls;
|
||||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||||
@@ -177,13 +211,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() {
|
|||||||
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
||||||
size_t at = this->rx_buffer_.size();
|
size_t at = this->rx_buffer_.size();
|
||||||
this->rx_buffer_.push_back(byte);
|
this->rx_buffer_.push_back(byte);
|
||||||
ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte);
|
ESP_LOGV(TAG, "Byte: 0x%02X", byte);
|
||||||
const uint8_t *raw = &this->rx_buffer_[0];
|
const uint8_t *raw = &this->rx_buffer_[0];
|
||||||
|
|
||||||
return improv::parse_improv_serial_byte(
|
return improv::parse_improv_serial_byte(
|
||||||
at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
|
at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); },
|
||||||
[this](improv::Error error) -> void {
|
[this](improv::Error error) -> void {
|
||||||
ESP_LOGW(TAG, "Error decoding Improv payload");
|
ESP_LOGW(TAG, "Error decoding payload");
|
||||||
this->set_error_(error);
|
this->set_error_(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -199,7 +233,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
wifi::global_wifi_component->set_sta(sta);
|
wifi::global_wifi_component->set_sta(sta);
|
||||||
wifi::global_wifi_component->start_connecting(sta, false);
|
wifi::global_wifi_component->start_connecting(sta, false);
|
||||||
this->set_state_(improv::STATE_PROVISIONING);
|
this->set_state_(improv::STATE_PROVISIONING);
|
||||||
ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||||
command.password.c_str());
|
command.password.c_str());
|
||||||
|
|
||||||
auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
|
auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this);
|
||||||
@@ -240,7 +274,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
ESP_LOGW(TAG, "Unknown Improv payload");
|
ESP_LOGW(TAG, "Unknown payload");
|
||||||
this->set_error_(improv::ERROR_UNKNOWN_RPC);
|
this->set_error_(improv::ERROR_UNKNOWN_RPC);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -249,57 +283,26 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
|||||||
|
|
||||||
void ImprovSerialComponent::set_state_(improv::State state) {
|
void ImprovSerialComponent::set_state_(improv::State state) {
|
||||||
this->state_ = state;
|
this->state_ = state;
|
||||||
|
this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE;
|
||||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
this->tx_header_[TX_DATA_IDX] = state;
|
||||||
data.resize(11);
|
this->write_data_();
|
||||||
data[6] = IMPROV_SERIAL_VERSION;
|
|
||||||
data[7] = TYPE_CURRENT_STATE;
|
|
||||||
data[8] = 1;
|
|
||||||
data[9] = state;
|
|
||||||
|
|
||||||
uint8_t checksum = 0x00;
|
|
||||||
for (uint8_t d : data)
|
|
||||||
checksum += d;
|
|
||||||
data[10] = checksum;
|
|
||||||
|
|
||||||
this->write_data_(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::set_error_(improv::Error error) {
|
void ImprovSerialComponent::set_error_(improv::Error error) {
|
||||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE;
|
||||||
data.resize(11);
|
this->tx_header_[TX_DATA_IDX] = error;
|
||||||
data[6] = IMPROV_SERIAL_VERSION;
|
this->write_data_();
|
||||||
data[7] = TYPE_ERROR_STATE;
|
|
||||||
data[8] = 1;
|
|
||||||
data[9] = error;
|
|
||||||
|
|
||||||
uint8_t checksum = 0x00;
|
|
||||||
for (uint8_t d : data)
|
|
||||||
checksum += d;
|
|
||||||
data[10] = checksum;
|
|
||||||
this->write_data_(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
|
void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) {
|
||||||
std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'};
|
this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE;
|
||||||
data.resize(9);
|
this->write_data_(response.data(), response.size());
|
||||||
data[6] = IMPROV_SERIAL_VERSION;
|
|
||||||
data[7] = TYPE_RPC_RESPONSE;
|
|
||||||
data[8] = response.size();
|
|
||||||
data.insert(data.end(), response.begin(), response.end());
|
|
||||||
|
|
||||||
uint8_t checksum = 0x00;
|
|
||||||
for (uint8_t d : data)
|
|
||||||
checksum += d;
|
|
||||||
data.push_back(checksum);
|
|
||||||
|
|
||||||
this->write_data_(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ImprovSerialComponent::on_wifi_connect_timeout_() {
|
void ImprovSerialComponent::on_wifi_connect_timeout_() {
|
||||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||||
this->set_state_(improv::STATE_AUTHORIZED);
|
this->set_state_(improv::STATE_AUTHORIZED);
|
||||||
ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
|
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
|
||||||
wifi::global_wifi_component->clear_sta();
|
wifi::global_wifi_component->clear_sta();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,16 @@
|
|||||||
namespace esphome {
|
namespace esphome {
|
||||||
namespace improv_serial {
|
namespace improv_serial {
|
||||||
|
|
||||||
|
// TX buffer layout constants
|
||||||
|
static constexpr uint8_t TX_HEADER_SIZE = 6; // Bytes 0-5 = "IMPROV"
|
||||||
|
static constexpr uint8_t TX_VERSION_IDX = 6;
|
||||||
|
static constexpr uint8_t TX_TYPE_IDX = 7;
|
||||||
|
static constexpr uint8_t TX_LENGTH_IDX = 8;
|
||||||
|
static constexpr uint8_t TX_DATA_IDX = 9; // For state/error messages only
|
||||||
|
static constexpr uint8_t TX_CHECKSUM_IDX = 10;
|
||||||
|
static constexpr uint8_t TX_NEWLINE_IDX = 11;
|
||||||
|
static constexpr uint8_t TX_BUFFER_SIZE = 12;
|
||||||
|
|
||||||
enum ImprovSerialType : uint8_t {
|
enum ImprovSerialType : uint8_t {
|
||||||
TYPE_CURRENT_STATE = 0x01,
|
TYPE_CURRENT_STATE = 0x01,
|
||||||
TYPE_ERROR_STATE = 0x02,
|
TYPE_ERROR_STATE = 0x02,
|
||||||
@@ -57,7 +67,22 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
|
|||||||
std::vector<uint8_t> build_version_info_();
|
std::vector<uint8_t> build_version_info_();
|
||||||
|
|
||||||
optional<uint8_t> read_byte_();
|
optional<uint8_t> read_byte_();
|
||||||
void write_data_(std::vector<uint8_t> &data);
|
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
|
||||||
|
|
||||||
|
uint8_t tx_header_[TX_BUFFER_SIZE] = {
|
||||||
|
'I', // 0: Header
|
||||||
|
'M', // 1: Header
|
||||||
|
'P', // 2: Header
|
||||||
|
'R', // 3: Header
|
||||||
|
'O', // 4: Header
|
||||||
|
'V', // 5: Header
|
||||||
|
IMPROV_SERIAL_VERSION, // 6: Version
|
||||||
|
0, // 7: ImprovSerialType
|
||||||
|
0, // 8: Length
|
||||||
|
0, // 9...X: Data (here, one byte reserved for state/error)
|
||||||
|
0, // X + 10: Checksum
|
||||||
|
'\n',
|
||||||
|
};
|
||||||
|
|
||||||
#ifdef USE_ESP32
|
#ifdef USE_ESP32
|
||||||
uart_port_t uart_num_;
|
uart_port_t uart_num_;
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Helper functions for lookups
|
// Helper functions for lookups
|
||||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
|
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) {
|
||||||
for (const auto &entry : arr) {
|
for (const auto &entry : arr) {
|
||||||
if (str == entry.str)
|
if (strcmp(str, entry.str) == 0)
|
||||||
return entry.value;
|
return entry.value;
|
||||||
}
|
}
|
||||||
return 0xFF; // Not found
|
return 0xFF; // Not found
|
||||||
@@ -441,7 +441,7 @@ bool LD2410Component::handle_ack_data_() {
|
|||||||
ESP_LOGV(TAG, "Baud rate change");
|
ESP_LOGV(TAG, "Baud rate change");
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
if (this->baud_rate_select_ != nullptr) {
|
if (this->baud_rate_select_ != nullptr) {
|
||||||
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
|
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
break;
|
break;
|
||||||
@@ -626,14 +626,14 @@ void LD2410Component::set_bluetooth(bool enable) {
|
|||||||
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
|
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void LD2410Component::set_distance_resolution(const std::string &state) {
|
void LD2410Component::set_distance_resolution(const char *state) {
|
||||||
this->set_config_mode_(true);
|
this->set_config_mode_(true);
|
||||||
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
|
const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00};
|
||||||
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
|
this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
|
||||||
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
|
this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void LD2410Component::set_baud_rate(const std::string &state) {
|
void LD2410Component::set_baud_rate(const char *state) {
|
||||||
this->set_config_mode_(true);
|
this->set_config_mode_(true);
|
||||||
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
|
const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
|
||||||
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
|
this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
|
||||||
@@ -759,10 +759,10 @@ void LD2410Component::set_light_out_control() {
|
|||||||
#endif
|
#endif
|
||||||
#ifdef USE_SELECT
|
#ifdef USE_SELECT
|
||||||
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
|
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
|
||||||
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state);
|
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
|
||||||
}
|
}
|
||||||
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
|
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
|
||||||
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state);
|
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option());
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
this->set_config_mode_(true);
|
this->set_config_mode_(true);
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
|
|||||||
void read_all_info();
|
void read_all_info();
|
||||||
void restart_and_read_all_info();
|
void restart_and_read_all_info();
|
||||||
void set_bluetooth(bool enable);
|
void set_bluetooth(bool enable);
|
||||||
void set_distance_resolution(const std::string &state);
|
void set_distance_resolution(const char *state);
|
||||||
void set_baud_rate(const std::string &state);
|
void set_baud_rate(const char *state);
|
||||||
void factory_reset();
|
void factory_reset();
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user