@@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro
|
||||
## 7. Specific Instructions for AI Collaboration
|
||||
|
||||
* **Contribution Workflow (Pull Request Process):**
|
||||
1. **Fork & Branch:** Create a new branch in your fork.
|
||||
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
|
||||
2. **Make Changes:** Adhere to all coding conventions and patterns.
|
||||
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
|
||||
4. **Lint:** Run `pre-commit` to ensure code is compliant.
|
||||
5. **Commit:** Commit your changes. There is no strict format for commit messages.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
|
||||
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
|
||||
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
|
||||
@@ -402,35 +402,45 @@ This document provides essential context for AI models interacting with this pro
|
||||
_use_feature = True
|
||||
```
|
||||
|
||||
**Good Pattern (CORE.data with Helpers):**
|
||||
**Bad Pattern (Flat Keys):**
|
||||
```python
|
||||
# Don't do this - keys should be namespaced under component domain
|
||||
MY_FEATURE_KEY = "my_component_feature"
|
||||
CORE.data[MY_FEATURE_KEY] = True
|
||||
```
|
||||
|
||||
**Good Pattern (dataclass):**
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
from esphome.core import CORE
|
||||
|
||||
# Keys for CORE.data storage
|
||||
COMPONENT_STATE_KEY = "my_component_state"
|
||||
USE_FEATURE_KEY = "my_component_use_feature"
|
||||
DOMAIN = "my_component"
|
||||
|
||||
def _get_component_state() -> list:
|
||||
"""Get component state from CORE.data."""
|
||||
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
|
||||
@dataclass
|
||||
class MyComponentData:
|
||||
feature_enabled: bool = False
|
||||
item_count: int = 0
|
||||
items: list[str] = field(default_factory=list)
|
||||
|
||||
def _get_use_feature() -> bool | None:
|
||||
"""Get feature flag from CORE.data."""
|
||||
return CORE.data.get(USE_FEATURE_KEY)
|
||||
def _get_data() -> MyComponentData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = MyComponentData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
def _set_use_feature(value: bool) -> None:
|
||||
"""Set feature flag in CORE.data."""
|
||||
CORE.data[USE_FEATURE_KEY] = value
|
||||
def request_feature() -> None:
|
||||
_get_data().feature_enabled = True
|
||||
|
||||
def enable_feature():
|
||||
_set_use_feature(True)
|
||||
def add_item(item: str) -> None:
|
||||
_get_data().items.append(item)
|
||||
```
|
||||
|
||||
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
|
||||
|
||||
**Why this matters:**
|
||||
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
||||
- `CORE.data` automatically clears between runs
|
||||
- Typed helper functions provide better IDE support and maintainability
|
||||
- Encapsulation makes state management explicit and testable
|
||||
- Namespacing under `DOMAIN` prevents key collisions between components
|
||||
- `@dataclass` provides type safety and cleaner attribute access
|
||||
|
||||
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
|
||||
766420905c06eeb6c5f360f68fd965e5ddd9c4a5db6b823263d3ad3accb64a07
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,6 +7,7 @@
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
|
||||
6
.github/workflows/auto-label-pr.yml
vendored
6
.github/workflows/auto-label-pr.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -68,6 +68,7 @@ jobs:
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'code-quality'
|
||||
];
|
||||
|
||||
@@ -367,6 +368,7 @@ jobs:
|
||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
|
||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
|
||||
52
.github/workflows/ci.yml
vendored
52
.github/workflows/ci.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -237,10 +237,10 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -577,7 +577,7 @@ jobs:
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -662,13 +662,13 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
env:
|
||||
SKIP: pylint,clang-tidy-hash
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
@@ -840,7 +840,7 @@ jobs:
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
steps:
|
||||
- name: Check out PR branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -959,13 +959,13 @@ jobs:
|
||||
- memory-impact-comment
|
||||
if: always()
|
||||
steps:
|
||||
- name: Success
|
||||
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||
run: exit 0
|
||||
- name: Failure
|
||||
if: ${{ contains(needs.*.result, 'failure') }}
|
||||
- name: Check job results
|
||||
env:
|
||||
JSON_DOC: ${{ toJSON(needs) }}
|
||||
NEEDS_JSON: ${{ toJSON(needs) }}
|
||||
run: |
|
||||
echo $JSON_DOC | jq
|
||||
exit 1
|
||||
# memory-impact-target-branch is allowed to fail without blocking CI.
|
||||
# This job builds the target branch (dev/beta/release) which may fail because:
|
||||
# 1. The target branch has a build issue independent of this PR
|
||||
# 2. This PR fixes a build issue on the target branch
|
||||
# In either case, we only care that the PR branch builds successfully.
|
||||
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -54,11 +54,11 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/init@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/analyze@cf1bb45a277cb3c205638b2cd5c984db1c46a412 # v4.31.7
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Build
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
|
||||
8
.github/workflows/sync-device-classes.yml
vendored
8
.github/workflows/sync-device-classes.yml
vendored
@@ -13,16 +13,16 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7.0.11
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.4
|
||||
rev: v0.14.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
12
CODEOWNERS
12
CODEOWNERS
@@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum
|
||||
esphome/components/addressable_light/* @justfalter
|
||||
esphome/components/ade7880/* @kpfleming
|
||||
esphome/components/ade7953/* @angelnu
|
||||
esphome/components/ade7953_base/* @angelnu
|
||||
esphome/components/ade7953_i2c/* @angelnu
|
||||
esphome/components/ade7953_spi/* @angelnu
|
||||
esphome/components/ads1118/* @solomondg1
|
||||
@@ -72,6 +73,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/ble_nus/* @tomaszduda23
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bm8563/* @abmantis
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -95,6 +97,7 @@ esphome/components/camera_encoder/* @DT-art1
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
@@ -188,6 +191,7 @@ esphome/components/gps/* @coogle @ximex
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/graphical_display_menu/* @MrMDavidson
|
||||
esphome/components/gree/* @orestismers
|
||||
esphome/components/gree/switch/* @nagyrobi
|
||||
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
||||
esphome/components/grove_tb6612fng/* @max246
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
@@ -202,11 +206,13 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hlk_fm22x/* @OnFreund
|
||||
esphome/components/hlw8032/* @rici4kubicek
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/hmac_md5/* @dwmw2
|
||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||
@@ -222,6 +228,7 @@ esphome/components/hte501/* @Stock-M
|
||||
esphome/components/http_request/ota/* @oarcher
|
||||
esphome/components/http_request/update/* @jesserockz
|
||||
esphome/components/htu31d/* @betterengineering
|
||||
esphome/components/hub75/* @stuartparmenter
|
||||
esphome/components/hydreon_rgxx/* @functionpointer
|
||||
esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
@@ -301,7 +308,7 @@ esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @jorre05
|
||||
esphome/components/micronova/* @edenhaus @jorre05
|
||||
esphome/components/microphone/* @jesserockz @kahrendt
|
||||
esphome/components/mics_4514/* @jesserockz
|
||||
esphome/components/midea/* @dudanov
|
||||
@@ -460,6 +467,7 @@ esphome/components/st7735/* @SenexCrenshaw
|
||||
esphome/components/st7789v/* @kbx81
|
||||
esphome/components/st7920/* @marsjan155
|
||||
esphome/components/statsd/* @Links2004
|
||||
esphome/components/stts22h/* @B48D81EFCC
|
||||
esphome/components/substitutions/* @esphome/core
|
||||
esphome/components/sun/* @OttoWinter
|
||||
esphome/components/sun_gtil2/* @Mat931
|
||||
@@ -481,6 +489,7 @@ esphome/components/template/datetime/* @rfdarter
|
||||
esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
@@ -515,6 +524,7 @@ esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/update/* @jesserockz
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/usb_cdc_acm/* @kbx81
|
||||
esphome/components/usb_host/* @clydebarrow
|
||||
esphome/components/usb_uart/* @clydebarrow
|
||||
esphome/components/valve/* @esphome/core
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
We welcome contributions to the ESPHome suite of code and documentation!
|
||||
|
||||
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
|
||||
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the
|
||||
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
|
||||
|
||||
**See also:**
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.11.5
|
||||
PROJECT_NUMBER = 2025.12.0
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
|
||||
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg">
|
||||
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
|
||||
@@ -944,6 +944,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
"""
|
||||
from esphome import platformio_api
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
@@ -966,7 +967,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
external_components = detect_external_components(config)
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Perform memory analysis
|
||||
# Perform component memory analysis
|
||||
_LOGGER.info("Analyzing memory usage...")
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
str(firmware_elf),
|
||||
@@ -976,11 +977,28 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and display report
|
||||
# Generate and display component report
|
||||
report = analyzer.generate_report()
|
||||
print()
|
||||
print(report)
|
||||
|
||||
# Perform RAM strings analysis
|
||||
_LOGGER.info("Analyzing RAM strings...")
|
||||
try:
|
||||
ram_analyzer = RamStringsAnalyzer(
|
||||
str(firmware_elf),
|
||||
objdump_path=idedata.objdump_path,
|
||||
platform=CORE.target_platform,
|
||||
)
|
||||
ram_analyzer.analyze()
|
||||
|
||||
# Generate and display RAM strings report
|
||||
ram_report = ram_analyzer.generate_report()
|
||||
print()
|
||||
print(ram_report)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.warning("RAM strings analysis failed: %s", e)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
SECTION_TO_ATTR,
|
||||
SYMBOL_PATTERNS,
|
||||
)
|
||||
from .demangle import batch_demangle
|
||||
from .helpers import (
|
||||
get_component_class_patterns,
|
||||
get_esphome_components,
|
||||
@@ -27,15 +28,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# GCC global constructor/destructor prefix annotations
|
||||
_GCC_PREFIX_ANNOTATIONS = {
|
||||
"_GLOBAL__sub_I_": "global constructor for",
|
||||
"_GLOBAL__sub_D_": "global destructor for",
|
||||
}
|
||||
|
||||
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
|
||||
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
|
||||
|
||||
# C++ runtime patterns for categorization
|
||||
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
|
||||
|
||||
@@ -312,168 +304,9 @@ class MemoryAnalyzer:
|
||||
if not symbols:
|
||||
return
|
||||
|
||||
# Try to find the appropriate c++filt for the platform
|
||||
cppfilt_cmd = "c++filt"
|
||||
|
||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
||||
_LOGGER.debug("objdump_path = %s", self.objdump_path)
|
||||
|
||||
# Check if we have a toolchain-specific c++filt
|
||||
if self.objdump_path and self.objdump_path != "objdump":
|
||||
# Replace objdump with c++filt in the path
|
||||
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
|
||||
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
|
||||
if Path(potential_cppfilt).exists():
|
||||
cppfilt_cmd = potential_cppfilt
|
||||
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"✗ Toolchain c++filt not found at %s, using system c++filt",
|
||||
potential_cppfilt,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
|
||||
|
||||
# Strip GCC optimization suffixes and prefixes before demangling
|
||||
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
|
||||
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
|
||||
symbols_stripped: list[str] = []
|
||||
symbols_prefixes: list[str] = [] # Track removed prefixes
|
||||
for symbol in symbols:
|
||||
# Remove GCC optimization markers
|
||||
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
|
||||
|
||||
# Handle GCC global constructor/initializer prefixes
|
||||
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
|
||||
prefix = ""
|
||||
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
|
||||
if stripped.startswith(gcc_prefix):
|
||||
prefix = gcc_prefix
|
||||
stripped = stripped[len(prefix) :]
|
||||
break
|
||||
|
||||
symbols_stripped.append(stripped)
|
||||
symbols_prefixes.append(prefix)
|
||||
|
||||
try:
|
||||
# Send all symbols to c++filt at once
|
||||
result = subprocess.run(
|
||||
[cppfilt_cmd],
|
||||
input="\n".join(symbols_stripped),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
|
||||
# On error, cache originals
|
||||
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
|
||||
for symbol in symbols:
|
||||
self._demangle_cache[symbol] = symbol
|
||||
return
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.warning(
|
||||
"c++filt exited with code %d: %s",
|
||||
result.returncode,
|
||||
result.stderr[:200] if result.stderr else "(no error output)",
|
||||
)
|
||||
# Cache originals on failure
|
||||
for symbol in symbols:
|
||||
self._demangle_cache[symbol] = symbol
|
||||
return
|
||||
|
||||
# Process demangled output
|
||||
self._process_demangled_output(
|
||||
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
|
||||
)
|
||||
|
||||
def _process_demangled_output(
|
||||
self,
|
||||
symbols: list[str],
|
||||
symbols_stripped: list[str],
|
||||
symbols_prefixes: list[str],
|
||||
demangled_output: str,
|
||||
cppfilt_cmd: str,
|
||||
) -> None:
|
||||
"""Process demangled symbol output and populate cache.
|
||||
|
||||
Args:
|
||||
symbols: Original symbol names
|
||||
symbols_stripped: Stripped symbol names sent to c++filt
|
||||
symbols_prefixes: Removed prefixes to restore
|
||||
demangled_output: Output from c++filt
|
||||
cppfilt_cmd: Path to c++filt command (for logging)
|
||||
"""
|
||||
demangled_lines = demangled_output.strip().split("\n")
|
||||
failed_count = 0
|
||||
|
||||
for original, stripped, prefix, demangled in zip(
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||
):
|
||||
# Add back any prefix that was removed
|
||||
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
|
||||
|
||||
# If we stripped a suffix, add it back to the demangled name for clarity
|
||||
if original != stripped and not prefix:
|
||||
demangled = self._restore_symbol_suffix(original, demangled)
|
||||
|
||||
self._demangle_cache[original] = demangled
|
||||
|
||||
# Log symbols that failed to demangle (stayed the same as stripped version)
|
||||
if stripped == demangled and stripped.startswith("_Z"):
|
||||
failed_count += 1
|
||||
if failed_count <= 5: # Only log first 5 failures
|
||||
_LOGGER.warning("Failed to demangle: %s", original)
|
||||
|
||||
if failed_count == 0:
|
||||
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
|
||||
return
|
||||
|
||||
_LOGGER.warning(
|
||||
"Failed to demangle %d/%d symbols using %s",
|
||||
failed_count,
|
||||
len(symbols),
|
||||
cppfilt_cmd,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
|
||||
"""Restore prefix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
|
||||
stripped: Stripped symbol name
|
||||
demangled: Demangled symbol name
|
||||
|
||||
Returns:
|
||||
Demangled name with prefix restored/annotated
|
||||
"""
|
||||
if not prefix:
|
||||
return demangled
|
||||
|
||||
# Successfully demangled - add descriptive prefix
|
||||
if demangled != stripped and (
|
||||
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
|
||||
):
|
||||
return f"[{annotation}: {demangled}]"
|
||||
|
||||
# Failed to demangle - restore original prefix
|
||||
return prefix + demangled
|
||||
|
||||
@staticmethod
|
||||
def _restore_symbol_suffix(original: str, demangled: str) -> str:
|
||||
"""Restore GCC optimization suffix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
original: Original symbol name with suffix
|
||||
demangled: Demangled symbol name without suffix
|
||||
|
||||
Returns:
|
||||
Demangled name with suffix annotation
|
||||
"""
|
||||
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
|
||||
return f"{demangled} [{suffix_match.group(1)}]"
|
||||
return demangled
|
||||
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
|
||||
|
||||
def _demangle_symbol(self, symbol: str) -> str:
|
||||
"""Get demangled C++ symbol name from cache."""
|
||||
|
||||
182
esphome/analyze_memory/demangle.py
Normal file
182
esphome/analyze_memory/demangle.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Symbol demangling utilities for memory analysis.
|
||||
|
||||
This module provides functions for demangling C++ symbol names using c++filt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from .toolchain import find_tool
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# GCC global constructor/destructor prefix annotations
|
||||
GCC_PREFIX_ANNOTATIONS = {
|
||||
"_GLOBAL__sub_I_": "global constructor for",
|
||||
"_GLOBAL__sub_D_": "global destructor for",
|
||||
}
|
||||
|
||||
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
|
||||
GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
|
||||
|
||||
|
||||
def _strip_gcc_annotations(symbol: str) -> tuple[str, str]:
|
||||
"""Strip GCC optimization suffixes and prefixes from a symbol.
|
||||
|
||||
Args:
|
||||
symbol: The mangled symbol name
|
||||
|
||||
Returns:
|
||||
Tuple of (stripped_symbol, removed_prefix)
|
||||
"""
|
||||
# Remove GCC optimization markers
|
||||
stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
|
||||
|
||||
# Handle GCC global constructor/initializer prefixes
|
||||
prefix = ""
|
||||
for gcc_prefix in GCC_PREFIX_ANNOTATIONS:
|
||||
if stripped.startswith(gcc_prefix):
|
||||
prefix = gcc_prefix
|
||||
stripped = stripped[len(prefix) :]
|
||||
break
|
||||
|
||||
return stripped, prefix
|
||||
|
||||
|
||||
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
|
||||
"""Restore prefix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
|
||||
stripped: Stripped symbol name
|
||||
demangled: Demangled symbol name
|
||||
|
||||
Returns:
|
||||
Demangled name with prefix restored/annotated
|
||||
"""
|
||||
if not prefix:
|
||||
return demangled
|
||||
|
||||
# Successfully demangled - add descriptive prefix
|
||||
if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)):
|
||||
return f"[{annotation}: {demangled}]"
|
||||
|
||||
# Failed to demangle - restore original prefix
|
||||
return prefix + demangled
|
||||
|
||||
|
||||
def _restore_symbol_suffix(original: str, demangled: str) -> str:
|
||||
"""Restore GCC optimization suffix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
original: Original symbol name with suffix
|
||||
demangled: Demangled symbol name without suffix
|
||||
|
||||
Returns:
|
||||
Demangled name with suffix annotation
|
||||
"""
|
||||
if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
|
||||
return f"{demangled} [{suffix_match.group(1)}]"
|
||||
return demangled
|
||||
|
||||
|
||||
def batch_demangle(
|
||||
symbols: list[str],
|
||||
cppfilt_path: str | None = None,
|
||||
objdump_path: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Batch demangle C++ symbol names.
|
||||
|
||||
Args:
|
||||
symbols: List of symbol names to demangle
|
||||
cppfilt_path: Path to c++filt binary (auto-detected if not provided)
|
||||
objdump_path: Path to objdump binary to derive c++filt path from
|
||||
|
||||
Returns:
|
||||
Dictionary mapping original symbol names to demangled names
|
||||
"""
|
||||
cache: dict[str, str] = {}
|
||||
|
||||
if not symbols:
|
||||
return cache
|
||||
|
||||
# Find c++filt tool
|
||||
cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path)
|
||||
if not cppfilt_cmd:
|
||||
_LOGGER.warning("Could not find c++filt, symbols will not be demangled")
|
||||
return {s: s for s in symbols}
|
||||
|
||||
_LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd)
|
||||
|
||||
# Strip GCC optimization suffixes and prefixes before demangling
|
||||
symbols_stripped: list[str] = []
|
||||
symbols_prefixes: list[str] = []
|
||||
for symbol in symbols:
|
||||
stripped, prefix = _strip_gcc_annotations(symbol)
|
||||
symbols_stripped.append(stripped)
|
||||
symbols_prefixes.append(prefix)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[cppfilt_cmd],
|
||||
input="\n".join(symbols_stripped),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
|
||||
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.warning(
|
||||
"c++filt exited with code %d: %s",
|
||||
result.returncode,
|
||||
result.stderr[:200] if result.stderr else "(no error output)",
|
||||
)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
# Process demangled output
|
||||
demangled_lines = result.stdout.strip().split("\n")
|
||||
|
||||
# Check for output length mismatch
|
||||
if len(demangled_lines) != len(symbols):
|
||||
_LOGGER.warning(
|
||||
"c++filt output mismatch: expected %d lines, got %d",
|
||||
len(symbols),
|
||||
len(demangled_lines),
|
||||
)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
failed_count = 0
|
||||
|
||||
for original, stripped, prefix, demangled in zip(
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||
):
|
||||
# Add back any prefix that was removed
|
||||
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
||||
|
||||
# If we stripped a suffix, add it back to the demangled name for clarity
|
||||
if original != stripped and not prefix:
|
||||
demangled = _restore_symbol_suffix(original, demangled)
|
||||
|
||||
cache[original] = demangled
|
||||
|
||||
# Count symbols that failed to demangle
|
||||
if stripped == demangled and stripped.startswith("_Z"):
|
||||
failed_count += 1
|
||||
if failed_count <= 5:
|
||||
_LOGGER.debug("Failed to demangle: %s", original)
|
||||
|
||||
if failed_count > 0:
|
||||
_LOGGER.debug(
|
||||
"Failed to demangle %d/%d symbols using %s",
|
||||
failed_count,
|
||||
len(symbols),
|
||||
cppfilt_cmd,
|
||||
)
|
||||
|
||||
return cache
|
||||
493
esphome/analyze_memory/ram_strings.py
Normal file
493
esphome/analyze_memory/ram_strings.py
Normal file
@@ -0,0 +1,493 @@
|
||||
"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files.
|
||||
|
||||
This module identifies strings that are stored in RAM sections (.data, .bss, .rodata)
|
||||
rather than in flash sections (.irom0.text, .irom.text), which is important for
|
||||
memory-constrained platforms like ESP8266.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from .demangle import batch_demangle
|
||||
from .toolchain import find_tool
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ESP8266: .rodata is in RAM (DRAM), not flash
|
||||
# ESP32: .rodata is in flash, mapped to data bus
|
||||
ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"])
|
||||
ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"])
|
||||
|
||||
# ESP32: .rodata is memory-mapped from flash
|
||||
ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"])
|
||||
ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"])
|
||||
|
||||
# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss)
|
||||
DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class SectionInfo:
|
||||
"""Information about an ELF section."""
|
||||
|
||||
name: str
|
||||
address: int
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class RamString:
|
||||
"""A string found in RAM."""
|
||||
|
||||
section: str
|
||||
address: int
|
||||
content: str
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
"""Size in bytes including null terminator."""
|
||||
return len(self.content) + 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class RamSymbol:
|
||||
"""A symbol found in RAM."""
|
||||
|
||||
name: str
|
||||
sym_type: str
|
||||
address: int
|
||||
size: int
|
||||
section: str
|
||||
demangled: str = "" # Demangled name, set after batch demangling
|
||||
|
||||
|
||||
class RamStringsAnalyzer:
|
||||
"""Analyzes ELF files to find strings stored in RAM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
elf_path: str,
|
||||
objdump_path: str | None = None,
|
||||
min_length: int = 8,
|
||||
platform: str = "esp32",
|
||||
) -> None:
|
||||
"""Initialize the RAM strings analyzer.
|
||||
|
||||
Args:
|
||||
elf_path: Path to the ELF file to analyze
|
||||
objdump_path: Path to objdump binary (used to find other tools)
|
||||
min_length: Minimum string length to report (default: 8)
|
||||
platform: Platform name ("esp8266", "esp32", etc.) for section mapping
|
||||
"""
|
||||
self.elf_path = Path(elf_path)
|
||||
if not self.elf_path.exists():
|
||||
raise FileNotFoundError(f"ELF file not found: {elf_path}")
|
||||
|
||||
self.objdump_path = objdump_path
|
||||
self.min_length = min_length
|
||||
self.platform = platform
|
||||
|
||||
# Set RAM/flash sections based on platform
|
||||
if self.platform == "esp8266":
|
||||
self.ram_sections = ESP8266_RAM_SECTIONS
|
||||
self.flash_sections = ESP8266_FLASH_SECTIONS
|
||||
else:
|
||||
# ESP32 and other platforms
|
||||
self.ram_sections = ESP32_RAM_SECTIONS
|
||||
self.flash_sections = ESP32_FLASH_SECTIONS
|
||||
|
||||
self.sections: dict[str, SectionInfo] = {}
|
||||
self.ram_strings: list[RamString] = []
|
||||
self.ram_symbols: list[RamSymbol] = []
|
||||
|
||||
def _run_command(self, cmd: list[str]) -> str:
|
||||
"""Run a command and return its output."""
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
_LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr)
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Command not found: %s", cmd[0])
|
||||
raise
|
||||
|
||||
def analyze(self) -> None:
|
||||
"""Perform the full RAM analysis."""
|
||||
self._parse_sections()
|
||||
self._extract_strings()
|
||||
self._analyze_symbols()
|
||||
self._demangle_symbols()
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
"""Parse section headers from ELF file."""
|
||||
objdump = find_tool("objdump", self.objdump_path)
|
||||
if not objdump:
|
||||
_LOGGER.error("Could not find objdump command")
|
||||
return
|
||||
|
||||
try:
|
||||
output = self._run_command([objdump, "-h", str(self.elf_path)])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return
|
||||
|
||||
# Parse section headers
|
||||
# Format: Idx Name Size VMA LMA File off Algn
|
||||
section_pattern = re.compile(
|
||||
r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)"
|
||||
)
|
||||
|
||||
for line in output.split("\n"):
|
||||
if match := section_pattern.match(line):
|
||||
name = match.group(1)
|
||||
size = int(match.group(2), 16)
|
||||
vma = int(match.group(3), 16)
|
||||
self.sections[name] = SectionInfo(name, vma, size)
|
||||
|
||||
def _extract_strings(self) -> None:
|
||||
"""Extract strings from RAM sections."""
|
||||
objdump = find_tool("objdump", self.objdump_path)
|
||||
if not objdump:
|
||||
return
|
||||
|
||||
for section_name in self.ram_sections:
|
||||
if section_name not in self.sections:
|
||||
continue
|
||||
|
||||
try:
|
||||
output = self._run_command(
|
||||
[objdump, "-s", "-j", section_name, str(self.elf_path)]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# Section may exist but have no content (e.g., .bss)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
strings = self._parse_hex_dump(output, section_name)
|
||||
self.ram_strings.extend(strings)
|
||||
|
||||
def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]:
|
||||
"""Parse hex dump output to extract strings.
|
||||
|
||||
Args:
|
||||
output: Output from objdump -s
|
||||
section_name: Name of the section being parsed
|
||||
|
||||
Returns:
|
||||
List of RamString objects
|
||||
"""
|
||||
strings: list[RamString] = []
|
||||
current_string = bytearray()
|
||||
string_start_addr = 0
|
||||
|
||||
for line in output.split("\n"):
|
||||
# Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................"
|
||||
match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
addr = int(match.group(1), 16)
|
||||
hex_data = match.group(2).strip()
|
||||
|
||||
# Convert hex to bytes
|
||||
hex_bytes = hex_data.split()
|
||||
byte_offset = 0
|
||||
for hex_chunk in hex_bytes:
|
||||
# Handle both byte-by-byte and word formats
|
||||
for i in range(0, len(hex_chunk), 2):
|
||||
byte_val = int(hex_chunk[i : i + 2], 16)
|
||||
if 0x20 <= byte_val <= 0x7E: # Printable ASCII
|
||||
if not current_string:
|
||||
string_start_addr = addr + byte_offset
|
||||
current_string.append(byte_val)
|
||||
else:
|
||||
if byte_val == 0 and len(current_string) >= self.min_length:
|
||||
# Found null terminator
|
||||
strings.append(
|
||||
RamString(
|
||||
section=section_name,
|
||||
address=string_start_addr,
|
||||
content=current_string.decode(
|
||||
"ascii", errors="ignore"
|
||||
),
|
||||
)
|
||||
)
|
||||
current_string = bytearray()
|
||||
byte_offset += 1
|
||||
|
||||
return strings
|
||||
|
||||
def _analyze_symbols(self) -> None:
|
||||
"""Analyze symbols in RAM sections."""
|
||||
nm = find_tool("nm", self.objdump_path)
|
||||
if not nm:
|
||||
return
|
||||
|
||||
try:
|
||||
output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return
|
||||
|
||||
for line in output.split("\n"):
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
|
||||
try:
|
||||
addr = int(parts[0], 16)
|
||||
size = int(parts[1], 16) if parts[1] != "?" else 0
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
sym_type = parts[2]
|
||||
name = " ".join(parts[3:])
|
||||
|
||||
# Filter for data symbols
|
||||
if sym_type not in DATA_SYMBOL_TYPES:
|
||||
continue
|
||||
|
||||
# Check if symbol is in a RAM section
|
||||
for section_name in self.ram_sections:
|
||||
if section_name not in self.sections:
|
||||
continue
|
||||
|
||||
section = self.sections[section_name]
|
||||
if section.address <= addr < section.address + section.size:
|
||||
self.ram_symbols.append(
|
||||
RamSymbol(
|
||||
name=name,
|
||||
sym_type=sym_type,
|
||||
address=addr,
|
||||
size=size,
|
||||
section=section_name,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
def _demangle_symbols(self) -> None:
|
||||
"""Batch demangle all RAM symbol names."""
|
||||
if not self.ram_symbols:
|
||||
return
|
||||
|
||||
# Collect all symbol names and demangle them
|
||||
symbol_names = [s.name for s in self.ram_symbols]
|
||||
demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path)
|
||||
|
||||
# Assign demangled names to symbols
|
||||
for symbol in self.ram_symbols:
|
||||
symbol.demangled = demangle_cache.get(symbol.name, symbol.name)
|
||||
|
||||
def _get_sections_size(self, section_names: frozenset[str]) -> int:
|
||||
"""Get total size of specified sections."""
|
||||
return sum(
|
||||
section.size
|
||||
for name, section in self.sections.items()
|
||||
if name in section_names
|
||||
)
|
||||
|
||||
def get_total_ram_usage(self) -> int:
|
||||
"""Get total RAM usage from RAM sections."""
|
||||
return self._get_sections_size(self.ram_sections)
|
||||
|
||||
def get_total_flash_usage(self) -> int:
|
||||
"""Get total flash usage from flash sections."""
|
||||
return self._get_sections_size(self.flash_sections)
|
||||
|
||||
def get_total_string_bytes(self) -> int:
|
||||
"""Get total bytes used by strings in RAM."""
|
||||
return sum(s.size for s in self.ram_strings)
|
||||
|
||||
def get_repeated_strings(self) -> list[tuple[str, int]]:
|
||||
"""Find strings that appear multiple times.
|
||||
|
||||
Returns:
|
||||
List of (string, count) tuples sorted by potential savings
|
||||
"""
|
||||
string_counts: dict[str, int] = defaultdict(int)
|
||||
for ram_string in self.ram_strings:
|
||||
string_counts[ram_string.content] += 1
|
||||
|
||||
return sorted(
|
||||
[(s, c) for s, c in string_counts.items() if c > 1],
|
||||
key=lambda x: x[1] * (len(x[0]) + 1),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_long_strings(self, min_len: int = 20) -> list[RamString]:
|
||||
"""Get strings longer than the specified length.
|
||||
|
||||
Args:
|
||||
min_len: Minimum string length
|
||||
|
||||
Returns:
|
||||
List of RamString objects sorted by length
|
||||
"""
|
||||
return sorted(
|
||||
[s for s in self.ram_strings if len(s.content) >= min_len],
|
||||
key=lambda x: len(x.content),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]:
|
||||
"""Get RAM symbols larger than the specified size.
|
||||
|
||||
Args:
|
||||
min_size: Minimum symbol size in bytes
|
||||
|
||||
Returns:
|
||||
List of RamSymbol objects sorted by size
|
||||
"""
|
||||
return sorted(
|
||||
[s for s in self.ram_symbols if s.size >= min_size],
|
||||
key=lambda x: x.size,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def generate_report(self, show_all_sections: bool = False) -> str:
|
||||
"""Generate a formatted RAM strings analysis report.
|
||||
|
||||
Args:
|
||||
show_all_sections: If True, show all sections, not just RAM
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
lines: list[str] = []
|
||||
table_width = 80
|
||||
|
||||
lines.append("=" * table_width)
|
||||
lines.append(
|
||||
f"RAM Strings Analysis ({self.platform.upper()})".center(table_width)
|
||||
)
|
||||
lines.append("=" * table_width)
|
||||
lines.append("")
|
||||
|
||||
# Section Analysis
|
||||
lines.append("SECTION ANALYSIS")
|
||||
lines.append("-" * table_width)
|
||||
lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}")
|
||||
lines.append("-" * table_width)
|
||||
|
||||
total_ram_usage = 0
|
||||
total_flash_usage = 0
|
||||
|
||||
for name, section in sorted(self.sections.items(), key=lambda x: x[1].address):
|
||||
if name in self.ram_sections:
|
||||
location = "RAM"
|
||||
total_ram_usage += section.size
|
||||
elif name in self.flash_sections:
|
||||
location = "FLASH"
|
||||
total_flash_usage += section.size
|
||||
else:
|
||||
location = "OTHER"
|
||||
|
||||
if show_all_sections or name in self.ram_sections:
|
||||
lines.append(
|
||||
f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}"
|
||||
)
|
||||
|
||||
lines.append("-" * table_width)
|
||||
lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes")
|
||||
lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes")
|
||||
|
||||
# Strings in RAM
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("STRINGS IN RAM SECTIONS")
|
||||
lines.append("=" * table_width)
|
||||
lines.append(
|
||||
"Note: .bss sections contain uninitialized data (no strings to extract)"
|
||||
)
|
||||
|
||||
# Group strings by section
|
||||
strings_by_section: dict[str, list[RamString]] = defaultdict(list)
|
||||
for ram_string in self.ram_strings:
|
||||
strings_by_section[ram_string.section].append(ram_string)
|
||||
|
||||
for section_name in sorted(strings_by_section.keys()):
|
||||
section_strings = strings_by_section[section_name]
|
||||
lines.append(f"\nSection: {section_name}")
|
||||
lines.append("-" * 40)
|
||||
for ram_string in sorted(section_strings, key=lambda x: x.address):
|
||||
clean_string = ram_string.content[:100] + (
|
||||
"..." if len(ram_string.content) > 100 else ""
|
||||
)
|
||||
lines.append(
|
||||
f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})'
|
||||
)
|
||||
|
||||
# Large RAM symbols
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)")
|
||||
lines.append("=" * table_width)
|
||||
|
||||
largest_symbols = self.get_largest_symbols(50)
|
||||
lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}")
|
||||
lines.append("-" * table_width)
|
||||
|
||||
for symbol in largest_symbols:
|
||||
# Use demangled name if available, otherwise raw name
|
||||
display_name = symbol.demangled or symbol.name
|
||||
name_display = display_name[:49] if len(display_name) > 49 else display_name
|
||||
lines.append(
|
||||
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
||||
)
|
||||
|
||||
# Summary
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("SUMMARY")
|
||||
lines.append("=" * table_width)
|
||||
lines.append(f"Total strings found in RAM: {len(self.ram_strings)}")
|
||||
total_string_bytes = self.get_total_string_bytes()
|
||||
lines.append(f"Total bytes used by strings: {total_string_bytes:,}")
|
||||
|
||||
# Optimization targets
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("POTENTIAL OPTIMIZATION TARGETS")
|
||||
lines.append("=" * table_width)
|
||||
|
||||
# Repeated strings
|
||||
repeated = self.get_repeated_strings()[:10]
|
||||
if repeated:
|
||||
lines.append("\nRepeated strings (could be deduplicated):")
|
||||
for string, count in repeated:
|
||||
savings = (count - 1) * (len(string) + 1)
|
||||
clean_string = string[:50] + ("..." if len(string) > 50 else "")
|
||||
lines.append(
|
||||
f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)'
|
||||
)
|
||||
|
||||
# Long strings - platform-specific advice
|
||||
long_strings = self.get_long_strings(20)[:10]
|
||||
if long_strings:
|
||||
if self.platform == "esp8266":
|
||||
lines.append(
|
||||
"\nLong strings that could be moved to PROGMEM (>= 20 chars):"
|
||||
)
|
||||
else:
|
||||
# ESP32: strings in DRAM are typically there for a reason
|
||||
# (interrupt handlers, pre-flash-init code, etc.)
|
||||
lines.append("\nLong strings in DRAM (>= 20 chars):")
|
||||
lines.append(
|
||||
"Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts"
|
||||
)
|
||||
for ram_string in long_strings:
|
||||
clean_string = ram_string.content[:60] + (
|
||||
"..." if len(ram_string.content) > 60 else ""
|
||||
)
|
||||
lines.append(
|
||||
f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)'
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
57
esphome/analyze_memory/toolchain.py
Normal file
57
esphome/analyze_memory/toolchain.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Toolchain utilities for memory analysis."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Platform-specific toolchain prefixes
|
||||
TOOLCHAIN_PREFIXES = [
|
||||
"xtensa-lx106-elf-", # ESP8266
|
||||
"xtensa-esp32-elf-", # ESP32
|
||||
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
||||
"", # System default (no prefix)
|
||||
]
|
||||
|
||||
|
||||
def find_tool(
|
||||
tool_name: str,
|
||||
objdump_path: str | None = None,
|
||||
) -> str | None:
|
||||
"""Find a toolchain tool by name.
|
||||
|
||||
First tries to derive the tool path from objdump_path (if provided),
|
||||
then falls back to searching for platform-specific tools.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
||||
objdump_path: Path to objdump binary to derive other tool paths from
|
||||
|
||||
Returns:
|
||||
Path to the tool or None if not found
|
||||
"""
|
||||
# Try to derive from objdump path first (most reliable)
|
||||
if objdump_path and objdump_path != "objdump":
|
||||
objdump_file = Path(objdump_path)
|
||||
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
|
||||
new_name = objdump_file.name.replace("objdump", tool_name)
|
||||
potential_path = str(objdump_file.with_name(new_name))
|
||||
if Path(potential_path).exists():
|
||||
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
||||
return potential_path
|
||||
|
||||
# Try platform-specific tools
|
||||
for prefix in TOOLCHAIN_PREFIXES:
|
||||
cmd = f"{prefix}{tool_name}"
|
||||
try:
|
||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||
_LOGGER.debug("Found %s: %s", tool_name, cmd)
|
||||
return cmd
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
_LOGGER.warning("Could not find %s tool", tool_name)
|
||||
return None
|
||||
@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
|
||||
break;
|
||||
default:
|
||||
this->publish_state(NAN);
|
||||
this->status_set_error("Invalid saturation vapor pressure equation selection!");
|
||||
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||
@@ -163,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||
}
|
||||
|
||||
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||
// H/T to https://esphome.io/cookbook/bme280_environment.html
|
||||
// H/T to https://esphome.io/cookbook/bme280_environment/
|
||||
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
|
||||
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||
// es = saturated vapor pressure (kPa)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32C61,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
@@ -99,6 +101,13 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
5: adc_channel_t.ADC_CHANNEL_5,
|
||||
6: adc_channel_t.ADC_CHANNEL_6,
|
||||
},
|
||||
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
|
||||
VARIANT_ESP32C61: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
3: adc_channel_t.ADC_CHANNEL_1,
|
||||
4: adc_channel_t.ADC_CHANNEL_2,
|
||||
5: adc_channel_t.ADC_CHANNEL_3,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -107,6 +116,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
4: adc_channel_t.ADC_CHANNEL_3,
|
||||
5: adc_channel_t.ADC_CHANNEL_4,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
|
||||
VARIANT_ESP32P4: {
|
||||
16: adc_channel_t.ADC_CHANNEL_0,
|
||||
17: adc_channel_t.ADC_CHANNEL_1,
|
||||
18: adc_channel_t.ADC_CHANNEL_2,
|
||||
19: adc_channel_t.ADC_CHANNEL_3,
|
||||
20: adc_channel_t.ADC_CHANNEL_4,
|
||||
21: adc_channel_t.ADC_CHANNEL_5,
|
||||
22: adc_channel_t.ADC_CHANNEL_6,
|
||||
23: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S2: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -133,16 +153,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
9: adc_channel_t.ADC_CHANNEL_8,
|
||||
10: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
16: adc_channel_t.ADC_CHANNEL_0,
|
||||
17: adc_channel_t.ADC_CHANNEL_1,
|
||||
18: adc_channel_t.ADC_CHANNEL_2,
|
||||
19: adc_channel_t.ADC_CHANNEL_3,
|
||||
20: adc_channel_t.ADC_CHANNEL_4,
|
||||
21: adc_channel_t.ADC_CHANNEL_5,
|
||||
22: adc_channel_t.ADC_CHANNEL_6,
|
||||
23: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
}
|
||||
|
||||
# pin to adc2 channel mapping
|
||||
@@ -173,8 +183,19 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
VARIANT_ESP32C5: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
|
||||
VARIANT_ESP32C6: {}, # no ADC2
|
||||
# ESP32-C61 has no ADC2
|
||||
VARIANT_ESP32C61: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
|
||||
VARIANT_ESP32P4: {
|
||||
49: adc_channel_t.ADC_CHANNEL_0,
|
||||
50: adc_channel_t.ADC_CHANNEL_1,
|
||||
51: adc_channel_t.ADC_CHANNEL_2,
|
||||
52: adc_channel_t.ADC_CHANNEL_3,
|
||||
53: adc_channel_t.ADC_CHANNEL_4,
|
||||
54: adc_channel_t.ADC_CHANNEL_5,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S2: {
|
||||
11: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -201,14 +222,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
19: adc_channel_t.ADC_CHANNEL_8,
|
||||
20: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
49: adc_channel_t.ADC_CHANNEL_0,
|
||||
50: adc_channel_t.ADC_CHANNEL_1,
|
||||
51: adc_channel_t.ADC_CHANNEL_2,
|
||||
52: adc_channel_t.ADC_CHANNEL_3,
|
||||
53: adc_channel_t.ADC_CHANNEL_4,
|
||||
54: adc_channel_t.ADC_CHANNEL_5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,10 +42,11 @@ void ADCSensor::setup() {
|
||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||
init_config.unit_id = this->adc_unit_;
|
||||
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
|
||||
// USE_ESP32_VARIANT_ESP32H2
|
||||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
|
||||
@@ -74,7 +75,7 @@ void ADCSensor::setup() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
// RISC-V variants and S3 use curve fitting calibration
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
@@ -111,7 +112,7 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -186,11 +187,11 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -219,7 +220,7 @@ float ADCSensor::sample_autorange_() {
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -231,7 +232,7 @@ float ADCSensor::sample_autorange_() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -266,7 +267,7 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@@ -288,7 +289,7 @@ float ADCSensor::sample_autorange_() {
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
|
||||
@@ -24,6 +24,8 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
|
||||
CONF_CURRENT_A = "current_a"
|
||||
CONF_CURRENT_B = "current_b"
|
||||
CONF_ACTIVE_POWER_A = "active_power_a"
|
||||
|
||||
@@ -83,7 +83,7 @@ void AHT10Component::setup() {
|
||||
void AHT10Component::restart_read_() {
|
||||
if (this->read_count_ == AHT10_ATTEMPTS) {
|
||||
this->read_count_ = 0;
|
||||
this->status_set_error("Reading timed out");
|
||||
this->status_set_error(LOG_STR("Reading timed out"));
|
||||
return;
|
||||
}
|
||||
this->read_count_++;
|
||||
|
||||
@@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
|
||||
|
||||
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
if (this->response_offset_ >= this->response_length_) {
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
|
||||
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
||||
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(),
|
||||
response[0], response[1], response[2], response[3], response[4]);
|
||||
return;
|
||||
}
|
||||
@@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
};
|
||||
|
||||
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
|
||||
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
|
||||
@@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
this->response_offset_ = 0;
|
||||
this->response_length_ = 0;
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str());
|
||||
break;
|
||||
}
|
||||
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
@@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
|
||||
this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str());
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
|
||||
this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
this->char_handle_, packet->length, packet->data,
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
|
||||
status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_ = 0;
|
||||
@@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
|
||||
void Am43::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
if (this->current_sensor_ == 0) {
|
||||
@@ -107,7 +104,7 @@ void Am43::update() {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_++;
|
||||
|
||||
@@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
@@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
|
||||
status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +149,7 @@ void Anova::update() {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
this->current_request_++;
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ from esphome.const import (
|
||||
CONF_SERVICE,
|
||||
CONF_SERVICES,
|
||||
CONF_TAG,
|
||||
CONF_THEN,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,17 +64,21 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
|
||||
"HomeAssistantActionResponseTrigger", automation.Trigger
|
||||
)
|
||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
|
||||
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
|
||||
APIUnregisterServiceCallAction = api_ns.class_(
|
||||
"APIUnregisterServiceCallAction", automation.Action
|
||||
)
|
||||
|
||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
|
||||
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
|
||||
SERVICE_ARG_NATIVE_TYPES = {
|
||||
"bool": bool,
|
||||
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
||||
"bool": cg.bool_,
|
||||
"int": cg.int32,
|
||||
"float": float,
|
||||
"float": cg.float_,
|
||||
"string": cg.std_string,
|
||||
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
|
||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||
"string[]": cg.FixedVector.template(cg.std_string)
|
||||
.operator("const")
|
||||
.operator("ref"),
|
||||
@@ -85,6 +90,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@@ -101,6 +107,85 @@ def validate_encryption_key(value):
|
||||
return value
|
||||
|
||||
|
||||
CONF_SUPPORTS_RESPONSE = "supports_response"
|
||||
|
||||
# Enum values in api::enums namespace
|
||||
enums_ns = api_ns.namespace("enums")
|
||||
SUPPORTS_RESPONSE_OPTIONS = {
|
||||
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
|
||||
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
|
||||
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
|
||||
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
|
||||
}
|
||||
|
||||
|
||||
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
"""Auto-detect supports_response based on api.respond usage in the action's then block.
|
||||
|
||||
- If api.respond with data found: set to "optional" (unless user explicitly set)
|
||||
- If api.respond without data found: set to "status" (unless user explicitly set)
|
||||
- If no api.respond found: set to "none" (unless user explicitly set)
|
||||
"""
|
||||
|
||||
def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]:
|
||||
"""Recursively scan actions for api.respond.
|
||||
|
||||
Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data
|
||||
"""
|
||||
found_any = False
|
||||
has_data_any = False
|
||||
|
||||
if isinstance(items, list):
|
||||
for item in items:
|
||||
found, has_data = scan_actions(item)
|
||||
if found:
|
||||
found_any = True
|
||||
has_data_any = has_data_any or has_data
|
||||
elif isinstance(items, dict):
|
||||
# Check if this is an api.respond action
|
||||
if "api.respond" in items:
|
||||
respond_config = items["api.respond"]
|
||||
has_data = isinstance(respond_config, dict) and "data" in respond_config
|
||||
return True, has_data
|
||||
# Recursively check all values
|
||||
for value in items.values():
|
||||
found, has_data = scan_actions(value)
|
||||
if found:
|
||||
found_any = True
|
||||
has_data_any = has_data_any or has_data
|
||||
|
||||
return found_any, has_data_any
|
||||
|
||||
then = config.get(CONF_THEN, [])
|
||||
action_name = config.get(CONF_ACTION)
|
||||
found, has_data = scan_actions(then)
|
||||
|
||||
# If user explicitly set supports_response, validate and use that
|
||||
if CONF_SUPPORTS_RESPONSE in config:
|
||||
user_value = config[CONF_SUPPORTS_RESPONSE]
|
||||
# Validate: "only" requires api.respond with data
|
||||
if user_value == "only" and not has_data:
|
||||
raise cv.Invalid(
|
||||
f"Action '{action_name}' has supports_response=only but no api.respond "
|
||||
"action with 'data:' was found. Use 'status' for responses without data, "
|
||||
"or add 'data:' to your api.respond action."
|
||||
)
|
||||
return config
|
||||
|
||||
# Auto-detect based on api.respond usage
|
||||
if found:
|
||||
config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status"
|
||||
else:
|
||||
config[CONF_SUPPORTS_RESPONSE] = "none"
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
ACTIONS_SCHEMA = automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
|
||||
@@ -111,10 +196,20 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
}
|
||||
),
|
||||
# No default - auto-detected by _auto_detect_supports_response
|
||||
cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum(
|
||||
SUPPORTS_RESPONSE_OPTIONS, lower=True
|
||||
),
|
||||
},
|
||||
cv.All(
|
||||
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
|
||||
cv.rename_key(CONF_SERVICE, CONF_ACTION),
|
||||
_auto_detect_supports_response,
|
||||
# Re-validate supports_response after auto-detection sets it
|
||||
cv.Schema(
|
||||
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
|
||||
extra=cv.ALLOW_EXTRA,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -151,7 +246,7 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||
_LOGGER.warning(
|
||||
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
|
||||
"Please migrate to the 'encryption' configuration. "
|
||||
"See https://esphome.io/components/api.html#configuration-variables"
|
||||
"See https://esphome.io/components/api/#configuration-variables"
|
||||
)
|
||||
|
||||
return config
|
||||
@@ -241,7 +336,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -260,9 +355,9 @@ async def to_code(config):
|
||||
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
# Set USE_API_SERVICES if any services are enabled
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
|
||||
|
||||
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
|
||||
if config[CONF_CUSTOM_SERVICES]:
|
||||
@@ -278,20 +373,61 @@ async def to_code(config):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
for conf in actions:
|
||||
template_args = []
|
||||
func_args = []
|
||||
service_arg_names = []
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
|
||||
# Determine supports_response mode
|
||||
# cv.enum returns the key with enum_value attribute containing the MockObj
|
||||
supports_response_key = conf[CONF_SUPPORTS_RESPONSE]
|
||||
supports_response = supports_response_key.enum_value
|
||||
is_none = supports_response_key == "none"
|
||||
is_optional = supports_response_key == "optional"
|
||||
|
||||
# Add call_id and return_response based on supports_response mode
|
||||
# These must match the C++ Trigger template arguments
|
||||
# - none: no extra args
|
||||
# - status: call_id only (for reporting success/error without data)
|
||||
# - only: call_id only (response always expected with data)
|
||||
# - optional: call_id + return_response (client decides)
|
||||
if not is_none:
|
||||
# call_id is present for "optional", "only", and "status"
|
||||
func_args.append((cg.uint32, "call_id"))
|
||||
# return_response only present for "optional"
|
||||
if is_optional:
|
||||
func_args.append((cg.bool_, "return_response"))
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
template_args.append(native)
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
templ = cg.TemplateArguments(*template_args)
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
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,
|
||||
)
|
||||
triggers.append(trigger)
|
||||
await automation.build_automation(trigger, func_args, conf)
|
||||
auto = await automation.build_automation(trigger, func_args, conf)
|
||||
|
||||
# For non-none response modes, automatically append unregister action
|
||||
# This ensures the call is unregistered after all actions complete (including async ones)
|
||||
if not is_none:
|
||||
arg_types = [arg[0] for arg in func_args]
|
||||
action_templ = cg.TemplateArguments(*arg_types)
|
||||
unregister_id = ID(
|
||||
f"{conf[CONF_TRIGGER_ID]}__unregister",
|
||||
is_declaration=True,
|
||||
type=APIUnregisterServiceCallAction.template(action_templ),
|
||||
)
|
||||
unregister_action = cg.new_Pvariable(
|
||||
unregister_id,
|
||||
var,
|
||||
)
|
||||
cg.add(auto.add_actions([unregister_action]))
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
|
||||
@@ -537,9 +673,98 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
|
||||
CONF_SUCCESS = "success"
|
||||
CONF_ERROR_MESSAGE = "error_message"
|
||||
|
||||
|
||||
def _validate_api_respond_data(config):
|
||||
"""Set flag during validation so AUTO_LOAD can include json component."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
return config
|
||||
|
||||
|
||||
API_RESPOND_ACTION_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_DATA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
_validate_api_respond_data,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"api.respond",
|
||||
APIRespondAction,
|
||||
API_RESPOND_ACTION_SCHEMA,
|
||||
)
|
||||
async def api_respond_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
# Validate that api.respond is used inside an API action context.
|
||||
# We can't easily validate this at config time since the schema validation
|
||||
# doesn't have access to the parent action context. Validating here in to_code
|
||||
# is still much better than a cryptic C++ compile error.
|
||||
has_call_id = any(name == "call_id" for _, name in args)
|
||||
if not has_call_id:
|
||||
raise EsphomeError(
|
||||
"api.respond can only be used inside an API action's 'then:' block. "
|
||||
"The 'call_id' variable is required to send a response."
|
||||
)
|
||||
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv)
|
||||
|
||||
# Check if we're in optional mode (has return_response arg)
|
||||
is_optional = any(name == "return_response" for _, name in args)
|
||||
if is_optional:
|
||||
cg.add(var.set_is_optional_mode(True))
|
||||
|
||||
templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_)
|
||||
cg.add(var.set_success(templ))
|
||||
|
||||
templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string)
|
||||
cg.add(var.set_error_message(templ))
|
||||
|
||||
if CONF_DATA in config:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON")
|
||||
# Lambda populates the JsonObject root - no return value needed
|
||||
lambda_ = await cg.process_lambda(
|
||||
config[CONF_DATA],
|
||||
args + [(cg.JsonObject, "root")],
|
||||
return_type=cg.void,
|
||||
)
|
||||
cg.add(var.set_data(lambda_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
|
||||
cv.boolean
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
return var
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
|
||||
@@ -518,7 +518,7 @@ message ListEntitiesLightResponse {
|
||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||
float min_mireds = 9;
|
||||
float max_mireds = 10;
|
||||
repeated string effects = 11;
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool disabled_by_default = 13;
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 15;
|
||||
@@ -579,7 +579,7 @@ message LightCommandRequest {
|
||||
bool has_flash_length = 16;
|
||||
uint32 flash_length = 17;
|
||||
bool has_effect = 18;
|
||||
string effect = 19;
|
||||
string effect = 19 [(pointer_to_buffer) = true];
|
||||
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -589,6 +589,7 @@ enum SensorStateClass {
|
||||
STATE_CLASS_MEASUREMENT = 1;
|
||||
STATE_CLASS_TOTAL_INCREASING = 2;
|
||||
STATE_CLASS_TOTAL = 3;
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4;
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.5
|
||||
@@ -854,22 +855,31 @@ enum ServiceArgType {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
||||
}
|
||||
enum SupportsResponseType {
|
||||
SUPPORTS_RESPONSE_NONE = 0;
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1;
|
||||
SUPPORTS_RESPONSE_ONLY = 2;
|
||||
// Status-only response - reports success/error without data payload
|
||||
// Value is higher to avoid conflicts with future Home Assistant values
|
||||
SUPPORTS_RESPONSE_STATUS = 100;
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
bool bool_ = 1;
|
||||
int32 legacy_int = 2;
|
||||
float float_ = 3;
|
||||
@@ -885,10 +895,25 @@ message ExecuteServiceRequest {
|
||||
option (id) = 42;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
fixed32 key = 1;
|
||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
}
|
||||
|
||||
// Message sent by ESPHome to Home Assistant with service execution response data
|
||||
message ExecuteServiceResponse {
|
||||
option (id) = 131;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
|
||||
|
||||
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
|
||||
bool success = 2; // Whether the service execution succeeded
|
||||
string error_message = 3; // Error message if success = false
|
||||
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// ==================== CAMERA ====================
|
||||
@@ -1170,7 +1195,7 @@ message SelectCommandRequest {
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2;
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,11 +6,17 @@
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#include <cerrno>
|
||||
#include <cinttypes>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
@@ -90,8 +96,8 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx->has_psk()) {
|
||||
auto &noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx.has_psk()) {
|
||||
this->helper_ =
|
||||
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
|
||||
} else {
|
||||
@@ -169,8 +175,7 @@ void APIConnection::loop() {
|
||||
} else {
|
||||
this->last_traffic_ = now;
|
||||
// read a packet
|
||||
this->read_message(buffer.data_len, buffer.type,
|
||||
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
|
||||
this->read_message(buffer.data_len, buffer.type, buffer.data);
|
||||
if (this->flags_.remove)
|
||||
return;
|
||||
}
|
||||
@@ -195,6 +200,9 @@ void APIConnection::loop() {
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,12 +492,16 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
msg.max_mireds = traits.get_max_mireds();
|
||||
}
|
||||
FixedVector<const char *> effects_list;
|
||||
if (light->supports_effects()) {
|
||||
msg.effects.emplace_back("None");
|
||||
for (auto *effect : light->get_effects()) {
|
||||
msg.effects.emplace_back(effect->get_name());
|
||||
auto &light_effects = light->get_effects();
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
effects_list.push_back(effect->get_name());
|
||||
}
|
||||
}
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -521,7 +533,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
if (msg.has_flash_length)
|
||||
call.set_flash_length(msg.flash_length);
|
||||
if (msg.has_effect)
|
||||
call.set_effect(msg.effect);
|
||||
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -893,7 +905,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(msg.state);
|
||||
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len);
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -1451,50 +1463,83 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_AREAS
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
#endif
|
||||
// mac_address must store temporary string - will be valid during send_message call
|
||||
std::string mac_address = get_mac_address_pretty();
|
||||
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char mac_address[18];
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, mac_address);
|
||||
resp.set_mac_address(StringRef(mac_address));
|
||||
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
|
||||
resp.set_compilation_time(App.get_compilation_time_ref());
|
||||
|
||||
// Compile-time StringRef constants for manufacturers
|
||||
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
|
||||
#define ESPHOME_MANUFACTURER "Espressif"
|
||||
#elif defined(USE_RP2040)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
|
||||
#define ESPHOME_MANUFACTURER "Raspberry Pi"
|
||||
#elif defined(USE_BK72XX)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||
#define ESPHOME_MANUFACTURER "Beken"
|
||||
#elif defined(USE_LN882X)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||
#define ESPHOME_MANUFACTURER "Lightning"
|
||||
#elif defined(USE_NRF52)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
|
||||
#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
|
||||
#elif defined(USE_RTL87XX)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||
#define ESPHOME_MANUFACTURER "Realtek"
|
||||
#elif defined(USE_HOST)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
|
||||
#define ESPHOME_MANUFACTURER "Host"
|
||||
#endif
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
|
||||
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
|
||||
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
|
||||
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
|
||||
resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
#endif
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
|
||||
char model_buf[sizeof(MODEL_PROGMEM)];
|
||||
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
|
||||
resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
|
||||
resp.set_model(MODEL);
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
|
||||
#endif
|
||||
#ifdef ESPHOME_PROJECT_NAME
|
||||
#ifdef USE_ESP8266
|
||||
static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
|
||||
static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
|
||||
char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
|
||||
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
|
||||
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
|
||||
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
|
||||
resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1));
|
||||
resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1));
|
||||
#else
|
||||
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
|
||||
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
|
||||
resp.set_project_name(PROJECT_NAME);
|
||||
resp.set_project_version(PROJECT_VERSION);
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
resp.webserver_port = USE_WEBSERVER_PORT;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
|
||||
// bt_mac must store temporary string - will be valid during send_message call
|
||||
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char bluetooth_mac[18];
|
||||
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
|
||||
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
@@ -1535,24 +1580,68 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
|
||||
// Compare entity_id and attribute with message fields
|
||||
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
|
||||
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
|
||||
(it.attribute == nullptr && msg.attribute.empty());
|
||||
|
||||
if (entity_match && attribute_match) {
|
||||
it.callback(msg.state);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Register the call and get a unique server-generated action_call_id
|
||||
// This avoids collisions when multiple clients use the same call_id
|
||||
uint32_t action_call_id = 0;
|
||||
if (msg.call_id != 0) {
|
||||
action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
|
||||
}
|
||||
// Use the overload that passes action_call_id separately (avoids copying msg)
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg, action_call_id)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#else
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
if (service->execute_service(msg)) {
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
if (!found) {
|
||||
ESP_LOGV(TAG, "Could not find service");
|
||||
}
|
||||
// Note: For services with supports_response != none, the call is unregistered
|
||||
// by an automatically appended APIUnregisterServiceCallAction at the end of
|
||||
// the action list. This ensures async actions (delays, waits) complete first.
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
resp.response_data = response_data;
|
||||
resp.response_data_len = response_data_len;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -1580,7 +1669,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Failed to clear encryption key");
|
||||
}
|
||||
} else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
} else if (base64_decode(msg.key, psk.data(), psk.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
@@ -1652,13 +1741,13 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Replace with new creator
|
||||
item.creator = std::move(creator);
|
||||
item.creator = creator;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// No existing item found, add new one
|
||||
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
|
||||
items.emplace_back(entity, creator, message_type, estimated_size);
|
||||
}
|
||||
|
||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
|
||||
@@ -1667,7 +1756,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre
|
||||
// This avoids expensive vector::insert which shifts all elements
|
||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
||||
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||
items.emplace_back(entity, std::move(creator), message_type, estimated_size);
|
||||
items.emplace_back(entity, creator, message_type, estimated_size);
|
||||
if (items.size() > 1) {
|
||||
// Swap the new high-priority item to the front
|
||||
std::swap(items.front(), items.back());
|
||||
@@ -1875,8 +1964,8 @@ void APIConnection::process_state_subscriptions_() {
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
|
||||
// Avoid string copy by directly using the optional's value if it exists
|
||||
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
|
||||
// Avoid string copy by using the const char* pointer if it exists
|
||||
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
|
||||
@@ -221,8 +221,15 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
@@ -505,28 +512,9 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
class MessageCreator {
|
||||
public:
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for const char * (Event types - no allocation needed)
|
||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
MessageCreator &operator=(const MessageCreator &other) = delete;
|
||||
|
||||
// Move constructor
|
||||
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
|
||||
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Call operator - uses message_type to determine union type
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
@@ -535,7 +523,7 @@ class APIConnection final : public APIServerConnection {
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
const char *const_char_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
|
||||
};
|
||||
|
||||
// Generic batching mechanism for both state updates and entity info
|
||||
@@ -548,16 +536,14 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
// Constructor for creating BatchItem
|
||||
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
|
||||
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
|
||||
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
|
||||
};
|
||||
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
// No pre-allocation - log connections never use batching, and for
|
||||
// connections that do, buffers are released after initial sync anyway
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
@@ -576,6 +562,15 @@ class APIConnection final : public APIServerConnection {
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
const BatchItem &operator[](size_t index) const { return items[index]; }
|
||||
// Release excess capacity - only releases if items already empty
|
||||
void release_buffer() {
|
||||
// Safe to call: batch is processed before release_buffer is called,
|
||||
// and if any items remain (partial processing), we must not clear them.
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
|
||||
if (items.empty()) {
|
||||
std::vector<BatchItem>().swap(items);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// DeferredBatch here (16 bytes, 4-byte aligned)
|
||||
@@ -709,12 +704,12 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
|
||||
// Fall back to scheduled batching
|
||||
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
|
||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
|
||||
@@ -35,10 +35,9 @@ struct ClientInfo;
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
struct ReadPacketBuffer {
|
||||
std::vector<uint8_t> container;
|
||||
uint16_t type;
|
||||
uint16_t data_offset;
|
||||
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
|
||||
uint16_t data_len;
|
||||
uint16_t type;
|
||||
};
|
||||
|
||||
// Packed packet info structure to minimize memory usage
|
||||
@@ -84,9 +83,7 @@ class APIFrameHelper {
|
||||
public:
|
||||
APIFrameHelper() = default;
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: socket_owned_(std::move(socket)), client_info_(client_info) {
|
||||
socket_ = socket_owned_.get();
|
||||
}
|
||||
: socket_(std::move(socket)), client_info_(client_info) {}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
@@ -121,6 +118,22 @@ class APIFrameHelper {
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
// rx_buf_: Safe to clear only if no partial read in progress.
|
||||
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
|
||||
// and clearing would lose partially received data.
|
||||
if (this->rx_buf_len_ == 0) {
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
|
||||
std::vector<uint8_t>().swap(this->rx_buf_);
|
||||
}
|
||||
// reusable_iovs_: Safe to release unconditionally.
|
||||
// Only used within write_protobuf_packets() calls - cleared at start,
|
||||
// populated with pointers, used for writev(), then function returns.
|
||||
// The iovecs contain stale pointers after the call (data was either sent
|
||||
// or copied to tx_buf_), and are cleared on next write_protobuf_packets().
|
||||
std::vector<struct iovec>().swap(this->reusable_iovs_);
|
||||
}
|
||||
|
||||
protected:
|
||||
// Buffer containing data to be sent
|
||||
@@ -149,9 +162,8 @@ class APIFrameHelper {
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
socket::Socket *socket_{nullptr};
|
||||
std::unique_ptr<socket::Socket> socket_owned_;
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
// Common state enum for all frame helpers
|
||||
// Note: Not all states are used by all implementations
|
||||
|
||||
@@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
|
||||
const std::string &name = App.get_name();
|
||||
const std::string &mac = get_mac_address();
|
||||
char mac[mac_len];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t mac_len = mac.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + mac_len;
|
||||
@@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
|
||||
std::memcpy(msg.get() + mac_offset, mac, mac_len);
|
||||
|
||||
aerr = write_frame_(msg.get(), total_size);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -406,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
buffer->container = std::move(this->rx_buf_);
|
||||
buffer->data_offset = 4;
|
||||
buffer->data = msg_data + 4; // Skip 4-byte header (type + length)
|
||||
buffer->data_len = data_len;
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
@@ -527,7 +527,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = ctx_->get_psk();
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
|
||||
APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
|
||||
@@ -9,9 +9,8 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||
const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
@@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
@@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return aerr;
|
||||
}
|
||||
|
||||
buffer->container = std::move(this->rx_buf_);
|
||||
buffer->data_offset = 0;
|
||||
buffer->data = this->rx_buf_.data();
|
||||
buffer->data_len = this->rx_header_parsed_len_;
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
|
||||
@@ -476,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
}
|
||||
buffer.encode_float(9, this->min_mireds);
|
||||
buffer.encode_float(10, this->max_mireds);
|
||||
for (auto &it : this->effects) {
|
||||
buffer.encode_string(11, it, true);
|
||||
for (const char *it : *this->effects) {
|
||||
buffer.encode_string(11, it, strlen(it), true);
|
||||
}
|
||||
buffer.encode_bool(13, this->disabled_by_default);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
@@ -499,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const {
|
||||
}
|
||||
size.add_float(1, this->min_mireds);
|
||||
size.add_float(1, this->max_mireds);
|
||||
if (!this->effects.empty()) {
|
||||
for (const auto &it : this->effects) {
|
||||
size.add_length_force(1, it.size());
|
||||
if (!this->effects->empty()) {
|
||||
for (const char *it : *this->effects) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
size.add_bool(1, this->disabled_by_default);
|
||||
@@ -611,9 +611,12 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 19:
|
||||
this->effect = value.as_string();
|
||||
case 19: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->effect = value.data();
|
||||
this->effect_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -995,7 +998,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->name_ref_);
|
||||
buffer.encode_uint32(2, static_cast<uint32_t>(this->type));
|
||||
@@ -1010,11 +1013,13 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
for (auto &it : this->args) {
|
||||
buffer.encode_message(3, it, true);
|
||||
}
|
||||
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_length(1, this->name_ref_.size());
|
||||
size.add_fixed32(1, this->key);
|
||||
size.add_repeated_message(1, this->args);
|
||||
size.add_uint32(1, static_cast<uint32_t>(this->supports_response));
|
||||
}
|
||||
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
@@ -1075,6 +1080,23 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
|
||||
this->string_array.init(count_string_array);
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
switch (field_id) {
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 3:
|
||||
this->call_id = value.as_uint32();
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
case 4:
|
||||
this->return_response = value.as_bool();
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
@@ -1102,6 +1124,24 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
|
||||
ProtoDecodableMessage::decode(buffer, length);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_uint32(1, this->call_id);
|
||||
buffer.encode_bool(2, this->success);
|
||||
buffer.encode_string(3, this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
buffer.encode_bytes(4, this->response_data, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
void ExecuteServiceResponse::calculate_size(ProtoSize &size) const {
|
||||
size.add_uint32(1, this->call_id);
|
||||
size.add_bool(1, this->success);
|
||||
size.add_length(1, this->error_message_ref_.size());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
size.add_length(4, this->response_data_len);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->object_id_ref_);
|
||||
@@ -1532,9 +1572,12 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
|
||||
}
|
||||
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2:
|
||||
this->state = value.as_string();
|
||||
case 2: {
|
||||
// Use raw data directly to avoid allocation
|
||||
this->state = value.data();
|
||||
this->state_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t {
|
||||
STATE_CLASS_MEASUREMENT = 1,
|
||||
STATE_CLASS_TOTAL_INCREASING = 2,
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4,
|
||||
};
|
||||
#endif
|
||||
enum LogLevel : uint32_t {
|
||||
@@ -63,7 +64,7 @@ enum LogLevel : uint32_t {
|
||||
LOG_LEVEL_VERBOSE = 6,
|
||||
LOG_LEVEL_VERY_VERBOSE = 7,
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_BOOL = 0,
|
||||
SERVICE_ARG_TYPE_INT = 1,
|
||||
@@ -74,6 +75,12 @@ enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
|
||||
};
|
||||
enum SupportsResponseType : uint32_t {
|
||||
SUPPORTS_RESPONSE_NONE = 0,
|
||||
SUPPORTS_RESPONSE_OPTIONAL = 1,
|
||||
SUPPORTS_RESPONSE_ONLY = 2,
|
||||
SUPPORTS_RESPONSE_STATUS = 100,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
enum ClimateMode : uint32_t {
|
||||
@@ -793,7 +800,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
||||
const light::ColorModeMask *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
const FixedVector<const char *> *effects{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -833,7 +840,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
|
||||
class LightCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 32;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 112;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 122;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "light_command_request"; }
|
||||
#endif
|
||||
@@ -862,7 +869,8 @@ class LightCommandRequest final : public CommandProtoMessage {
|
||||
bool has_flash_length{false};
|
||||
uint32_t flash_length{0};
|
||||
bool has_effect{false};
|
||||
std::string effect{};
|
||||
const uint8_t *effect{nullptr};
|
||||
uint16_t effect_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1239,7 +1247,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
StringRef name_ref_{};
|
||||
@@ -1256,7 +1264,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 41;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 48;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 50;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_services_response"; }
|
||||
#endif
|
||||
@@ -1264,6 +1272,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1296,12 +1305,18 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
|
||||
class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 42;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 39;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 45;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_request"; }
|
||||
#endif
|
||||
uint32_t key{0};
|
||||
FixedVector<ExecuteServiceArgument> args{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
uint32_t call_id{0};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
bool return_response{false};
|
||||
#endif
|
||||
void decode(const uint8_t *buffer, size_t length) override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
@@ -1310,6 +1325,32 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
|
||||
protected:
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
class ExecuteServiceResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 131;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 34;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "execute_service_response"; }
|
||||
#endif
|
||||
uint32_t call_id{0};
|
||||
bool success{false};
|
||||
StringRef error_message_ref_{};
|
||||
void set_error_message(const StringRef &ref) { this->error_message_ref_ = ref; }
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
const uint8_t *response_data{nullptr};
|
||||
uint16_t response_data_len{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
@@ -1564,11 +1605,12 @@ class SelectStateResponse final : public StateResponseProtoMessage {
|
||||
class SelectCommandRequest final : public CommandProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 54;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 18;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 28;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "select_command_request"; }
|
||||
#endif
|
||||
std::string state{};
|
||||
const uint8_t *state{nullptr};
|
||||
uint16_t state_len{0};
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
@@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in
|
||||
static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%llu", value);
|
||||
snprintf(buffer, 64, "%" PRIu64, value);
|
||||
append_with_newline(out, buffer);
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
|
||||
return "STATE_CLASS_TOTAL_INCREASING";
|
||||
case enums::STATE_CLASS_TOTAL:
|
||||
return "STATE_CLASS_TOTAL";
|
||||
case enums::STATE_CLASS_MEASUREMENT_ANGLE:
|
||||
return "STATE_CLASS_MEASUREMENT_ANGLE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -206,7 +208,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||
switch (value) {
|
||||
case enums::SERVICE_ARG_TYPE_BOOL:
|
||||
@@ -229,6 +231,20 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) {
|
||||
switch (value) {
|
||||
case enums::SUPPORTS_RESPONSE_NONE:
|
||||
return "SUPPORTS_RESPONSE_NONE";
|
||||
case enums::SUPPORTS_RESPONSE_OPTIONAL:
|
||||
return "SUPPORTS_RESPONSE_OPTIONAL";
|
||||
case enums::SUPPORTS_RESPONSE_ONLY:
|
||||
return "SUPPORTS_RESPONSE_ONLY";
|
||||
case enums::SUPPORTS_RESPONSE_STATUS:
|
||||
return "SUPPORTS_RESPONSE_STATUS";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
|
||||
@@ -924,7 +940,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
|
||||
}
|
||||
dump_field(out, "min_mireds", this->min_mireds);
|
||||
dump_field(out, "max_mireds", this->max_mireds);
|
||||
for (const auto &it : this->effects) {
|
||||
for (const auto &it : *this->effects) {
|
||||
dump_field(out, "effects", it, 4);
|
||||
}
|
||||
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
||||
@@ -983,7 +999,9 @@ void LightCommandRequest::dump_to(std::string &out) const {
|
||||
dump_field(out, "has_flash_length", this->has_flash_length);
|
||||
dump_field(out, "flash_length", this->flash_length);
|
||||
dump_field(out, "has_effect", this->has_effect);
|
||||
dump_field(out, "effect", this->effect);
|
||||
out.append(" effect: ");
|
||||
out.append(format_hex_pretty(this->effect, this->effect_len));
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
@@ -1177,7 +1195,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
|
||||
out.append(format_hex_pretty(this->timezone, this->timezone_len));
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
|
||||
dump_field(out, "name", this->name_ref_);
|
||||
@@ -1192,6 +1210,7 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
}
|
||||
void ExecuteServiceArgument::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceArgument");
|
||||
@@ -1221,6 +1240,25 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
dump_field(out, "return_response", this->return_response);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void ExecuteServiceResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ExecuteServiceResponse");
|
||||
dump_field(out, "call_id", this->call_id);
|
||||
dump_field(out, "success", this->success);
|
||||
dump_field(out, "error_message", this->error_message_ref_);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
out.append(" response_data: ");
|
||||
out.append(format_hex_pretty(this->response_data, this->response_data_len));
|
||||
out.append("\n");
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
@@ -1417,7 +1455,9 @@ void SelectStateResponse::dump_to(std::string &out) const {
|
||||
void SelectCommandRequest::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "SelectCommandRequest");
|
||||
dump_field(out, "key", this->key);
|
||||
dump_field(out, "state", this->state);
|
||||
out.append(" state: ");
|
||||
out.append(format_hex_pretty(this->state, this->state_len));
|
||||
out.append("\n");
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
|
||||
@@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
@@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
case ExecuteServiceRequest::MESSAGE_TYPE: {
|
||||
ExecuteServiceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
@@ -670,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
|
||||
this->subscribe_home_assistant_states(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
|
||||
@@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
|
||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
@@ -239,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -368,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
@@ -52,11 +52,6 @@ void APIServer::setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Schedule reboot if no clients connect within timeout
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
}
|
||||
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
@@ -101,42 +96,22 @@ void APIServer::setup() {
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
});
|
||||
logger::global_logger->add_log_listener(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
|
||||
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
});
|
||||
camera::Camera::instance()->add_listener(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::schedule_reboot_timeout_() {
|
||||
this->status_set_warning();
|
||||
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
|
||||
if (!global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
});
|
||||
// Initialize last_connected_ for reboot timeout tracking
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
// Set warning status if reboot timeout is enabled
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::loop() {
|
||||
@@ -164,15 +139,24 @@ void APIServer::loop() {
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// Clear warning status and cancel reboot when first client connects
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->cancel_timeout("api_reboot");
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,6 +186,9 @@ void APIServer::loop() {
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||
|
||||
@@ -211,9 +198,10 @@ void APIServer::loop() {
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Schedule reboot when last client disconnects
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
@@ -227,8 +215,8 @@ void APIServer::dump_config() {
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
|
||||
}
|
||||
#else
|
||||
@@ -431,25 +419,56 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper to add subscription (reduces duplication)
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f, bool once) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
|
||||
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
|
||||
});
|
||||
}
|
||||
|
||||
// Helper to add subscription with heap-allocated strings (reduces duplication)
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f, bool once) {
|
||||
HomeAssistantStateSubscription sub;
|
||||
// Allocate heap storage for the strings
|
||||
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
|
||||
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
|
||||
|
||||
if (attribute.has_value()) {
|
||||
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
|
||||
sub.attribute = sub.attribute_dynamic_storage->c_str();
|
||||
} else {
|
||||
sub.attribute = nullptr;
|
||||
}
|
||||
|
||||
sub.callback = std::move(f);
|
||||
sub.once = once;
|
||||
this->state_subs_.push_back(std::move(sub));
|
||||
}
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
||||
}
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = false,
|
||||
});
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = std::move(entity_id),
|
||||
.attribute = std::move(attribute),
|
||||
.callback = std::move(f),
|
||||
.once = true,
|
||||
});
|
||||
};
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
|
||||
return this->state_subs_;
|
||||
@@ -493,7 +512,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
ESP_LOGW(TAG, "Key set in YAML");
|
||||
return false;
|
||||
#else
|
||||
auto &old_psk = this->noise_ctx_->get_psk();
|
||||
auto &old_psk = this->noise_ctx_.get_psk();
|
||||
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
|
||||
ESP_LOGW(TAG, "New PSK matches old");
|
||||
return true;
|
||||
@@ -528,7 +547,42 @@ void APIServer::request_time() {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
|
||||
bool APIServer::is_connected(bool state_subscription_only) const {
|
||||
if (!state_subscription_only) {
|
||||
return !this->clients_.empty();
|
||||
}
|
||||
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServer::on_shutdown() {
|
||||
this->shutting_down_ = true;
|
||||
@@ -565,5 +619,84 @@ bool APIServer::teardown() {
|
||||
return this->clients_.empty();
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
|
||||
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
|
||||
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
|
||||
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
|
||||
#endif
|
||||
|
||||
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
|
||||
uint32_t action_call_id = this->next_action_call_id_++;
|
||||
// Handle wraparound (skip 0 as it means "no call")
|
||||
if (this->next_action_call_id_ == 0) {
|
||||
this->next_action_call_id_ = 1;
|
||||
}
|
||||
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
|
||||
|
||||
// Schedule automatic cleanup after timeout (client will have given up by then)
|
||||
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
|
||||
[this, action_call_id]() {
|
||||
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
|
||||
this->unregister_active_action_call(action_call_id);
|
||||
});
|
||||
|
||||
return action_call_id;
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
|
||||
|
||||
// Swap-and-pop is more efficient than remove_if for unordered vectors
|
||||
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
|
||||
if (this->active_action_calls_[i].action_call_id == action_call_id) {
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
|
||||
// Remove all active action calls for disconnected connection using swap-and-pop
|
||||
for (size_t i = 0; i < this->active_action_calls_.size();) {
|
||||
if (this->active_action_calls_[i].connection == conn) {
|
||||
// Cancel the timeout for this action call
|
||||
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
|
||||
|
||||
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
|
||||
this->active_action_calls_.pop_back();
|
||||
// Don't increment i - need to check the swapped element
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
|
||||
response_data_len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
||||
@@ -12,22 +12,39 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_SERVICES
|
||||
#include "user_services.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
// Forward declaration - full definition in user_services.h
|
||||
class UserServiceDescriptor;
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer : public Component, public Controller {
|
||||
class APIServer : public Component,
|
||||
public Controller
|
||||
#ifdef USE_LOGGER
|
||||
,
|
||||
public logger::LogListener
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
@@ -37,6 +54,12 @@ class APIServer : public Component, public Controller {
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
bool teardown() override;
|
||||
#ifdef USE_LOGGER
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool check_password(const uint8_t *password_data, size_t password_len) const;
|
||||
void set_password(const std::string &password);
|
||||
@@ -54,8 +77,8 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -124,7 +147,7 @@ class APIServer : public Component, public Controller {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
|
||||
this->user_services_.assign(services);
|
||||
}
|
||||
@@ -132,6 +155,19 @@ class APIServer : public Component, public Controller {
|
||||
// Only compile push_back method when custom_services: true (external components)
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Action call context management - supports concurrent calls from multiple clients
|
||||
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
|
||||
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
|
||||
void unregister_active_action_call(uint32_t action_call_id);
|
||||
void unregister_active_action_calls_for_connection(APIConnection *conn);
|
||||
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void request_time();
|
||||
@@ -150,23 +186,34 @@ class APIServer : public Component, public Controller {
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
|
||||
#endif
|
||||
|
||||
bool is_connected() const;
|
||||
bool is_connected(bool state_subscription_only = false) const;
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
std::string entity_id;
|
||||
optional<std::string> attribute;
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
|
||||
std::function<void(std::string)> callback;
|
||||
bool once;
|
||||
|
||||
// Dynamic storage for external components using std::string API (custom_api_device.h)
|
||||
// These are only allocated when using the std::string overload (nullptr for const char* overload)
|
||||
std::unique_ptr<std::string> entity_id_dynamic_storage;
|
||||
std::unique_ptr<std::string> attribute_dynamic_storage;
|
||||
};
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f);
|
||||
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
|
||||
@@ -180,11 +227,17 @@ class APIServer : public Component, public Controller {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
#endif // USE_API_NOISE
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper methods to reduce code duplication
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> f, bool once);
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
@@ -196,6 +249,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
// 4-byte aligned types
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
@@ -206,8 +260,19 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Active action calls - supports concurrent calls from multiple clients
|
||||
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
|
||||
struct ActiveActionCall {
|
||||
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
|
||||
uint32_t client_call_id; // Client's original call_id (used in response)
|
||||
APIConnection *connection;
|
||||
};
|
||||
std::vector<ActiveActionCall> active_action_calls_;
|
||||
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
struct PendingActionResponse {
|
||||
@@ -228,7 +293,7 @@ class APIServer : public Component, public Controller {
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||
APINoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
@@ -236,8 +301,11 @@ class APIServer : public Component, public Controller {
|
||||
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||
TEMPLATABLE_VALUE(bool, state_subscription_only)
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
|
||||
bool check(const Ts &...x) override {
|
||||
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <map>
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
|
||||
public:
|
||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||
@@ -16,12 +16,15 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
||||
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
|
||||
|
||||
protected:
|
||||
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
|
||||
// CustomAPIDevice services don't support action responses - ignore call_id and return_response
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
|
||||
(this->obj_->*this->callback_)(x...); // NOLINT
|
||||
}
|
||||
|
||||
T *obj_;
|
||||
void (T::*callback_)(Ts...);
|
||||
};
|
||||
#endif // USE_API_SERVICES
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
class CustomAPIDevice {
|
||||
public:
|
||||
@@ -49,7 +52,7 @@ class CustomAPIDevice {
|
||||
* @param name The name of the service to register.
|
||||
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
@@ -90,7 +93,7 @@ class CustomAPIDevice {
|
||||
* @param callback The member function to call when the service is triggered.
|
||||
* @param name The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
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
|
||||
|
||||
@@ -12,10 +12,17 @@
|
||||
#endif
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
|
||||
// Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation)
|
||||
// rather than being wrapped in a lambda. The base class constructor for const char* is more
|
||||
// specialized than the templated constructor here, so it should be selected.
|
||||
static_assert(std::is_constructible_v<TemplatableValue<std::string, X...>, const char *>,
|
||||
"Base class must have const char* constructor for STATIC_STRING optimization");
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
@@ -46,23 +53,25 @@ template<typename... Ts> class TemplatableKeyValuePair {
|
||||
|
||||
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
|
||||
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
|
||||
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
|
||||
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
|
||||
// Using const char* avoids std::string heap allocation - keys remain in flash.
|
||||
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {}
|
||||
|
||||
std::string key;
|
||||
const char *key{nullptr};
|
||||
TemplatableStringValue<Ts...> value;
|
||||
};
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
// Represents the response data from a Home Assistant action
|
||||
// Note: This class holds a StringRef to the error_message from the protobuf message.
|
||||
// The protobuf message must outlive the ActionResponse (which is guaranteed since
|
||||
// the callback is invoked synchronously while the message is on the stack).
|
||||
class ActionResponse {
|
||||
public:
|
||||
ActionResponse(bool success, std::string error_message = "")
|
||||
: success_(success), error_message_(std::move(error_message)) {}
|
||||
ActionResponse(bool success, const std::string &error_message) : success_(success), error_message_(error_message) {}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
|
||||
: success_(success), error_message_(std::move(error_message)) {
|
||||
ActionResponse(bool success, const std::string &error_message, const uint8_t *data, size_t data_len)
|
||||
: success_(success), error_message_(error_message) {
|
||||
if (data == nullptr || data_len == 0)
|
||||
return;
|
||||
this->json_document_ = json::parse_json(data, data_len);
|
||||
@@ -70,7 +79,8 @@ class ActionResponse {
|
||||
#endif
|
||||
|
||||
bool is_success() const { return this->success_; }
|
||||
const std::string &get_error_message() const { return this->error_message_; }
|
||||
// Returns reference to error message - can be implicitly converted to std::string if needed
|
||||
const StringRef &get_error_message() const { return this->error_message_; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
// Get data as parsed JSON object (const version returns read-only view)
|
||||
@@ -79,7 +89,7 @@ class ActionResponse {
|
||||
|
||||
protected:
|
||||
bool success_;
|
||||
std::string error_message_;
|
||||
StringRef error_message_;
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
JsonDocument json_document_;
|
||||
#endif
|
||||
@@ -105,14 +115,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
|
||||
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
|
||||
// The value parameter can be a lambda/template, but keys are never templatable.
|
||||
template<typename K, typename V> void add_data(K &&key, V &&value) {
|
||||
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
|
||||
// Using const char* for keys avoids std::string heap allocation - keys remain in flash.
|
||||
template<typename V> void add_data(const char *key, V &&value) {
|
||||
this->add_kv_(this->data_, key, std::forward<V>(value));
|
||||
}
|
||||
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
|
||||
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
|
||||
template<typename V> void add_data_template(const char *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, key, std::forward<V>(value));
|
||||
}
|
||||
template<typename K, typename V> void add_variable(K &&key, V &&value) {
|
||||
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
|
||||
template<typename V> void add_variable(const char *key, V &&value) {
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -185,10 +196,11 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
protected:
|
||||
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
|
||||
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
|
||||
// Helper to add key-value pairs to FixedVectors
|
||||
// Keys are always string literals (const char*), values can be lambdas/templates
|
||||
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
|
||||
auto &kv = vec.emplace_back();
|
||||
kv.key = std::forward<K>(key);
|
||||
kv.key = key;
|
||||
kv.value = std::forward<V>(value);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/util.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -82,7 +85,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
|
||||
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
|
||||
|
||||
@@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool on_service(UserServiceDescriptor *service) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
||||
@@ -846,7 +846,7 @@ class ProtoService {
|
||||
*/
|
||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||
|
||||
@@ -1,20 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#include "esphome/components/json/json_util.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
namespace esphome::api {
|
||||
|
||||
// Forward declaration - full definition in api_server.h
|
||||
class APIServer;
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Overload that accepts server-generated action_call_id (avoids client call_id collisions)
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0;
|
||||
#endif
|
||||
|
||||
bool is_internal() { return false; }
|
||||
};
|
||||
@@ -27,8 +38,9 @@ template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
// Stores only pointers to string literals in flash - no heap allocation
|
||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: name_(name), arg_names_(arg_names) {
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
|
||||
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
|
||||
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
|
||||
this->key_ = fnv1_hash(name);
|
||||
}
|
||||
|
||||
@@ -36,6 +48,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
@@ -51,20 +64,37 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#else
|
||||
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
|
||||
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Pointers to string literals in flash - no heap allocation
|
||||
const char *name_;
|
||||
std::array<const char *, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
|
||||
};
|
||||
|
||||
// Separate class for custom_api_device services (rare case)
|
||||
@@ -80,6 +110,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
@@ -95,14 +126,31 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#else
|
||||
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Dynamic services don't support responses yet, but need to implement the interface
|
||||
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
|
||||
if (req.key != this->key_)
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
|
||||
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Heap-allocated strings for runtime-generated names
|
||||
@@ -111,15 +159,149 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
uint32_t key_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
// Primary template declaration
|
||||
template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
|
||||
|
||||
// Specialization for NONE - no extra trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> : public UserServiceBase<Ts...>, public Trigger<Ts...> {
|
||||
public:
|
||||
// Constructor for static names (YAML-defined services - used by code generator)
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names) {}
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
|
||||
|
||||
protected:
|
||||
void execute(Ts... x) override { this->trigger(x...); } // NOLINT
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
|
||||
};
|
||||
|
||||
// Specialization for OPTIONAL - call_id and return_response trigger arguments
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, bool, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool return_response, Ts... x) override {
|
||||
this->trigger(call_id, return_response, x...);
|
||||
}
|
||||
};
|
||||
|
||||
// Specialization for ONLY - just call_id trigger argument
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
};
|
||||
|
||||
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
|
||||
template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_SERVICES
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
// Include full definition of APIServer for template implementation
|
||||
// Must be outside namespace to avoid including STL headers inside namespace
|
||||
#include "api_server.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
|
||||
|
||||
template<typename V> void set_success(V success) { this->success_ = success; }
|
||||
template<typename V> void set_error_message(V error) { this->error_message_ = error; }
|
||||
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void set_data(std::function<void(Ts..., JsonObject)> func) {
|
||||
this->json_builder_ = std::move(func);
|
||||
this->has_data_ = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - it's always first for optional/only/status modes
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
|
||||
bool success = this->success_.value(x...);
|
||||
std::string error_message = this->error_message_.value(x...);
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
if (this->has_data_) {
|
||||
// For optional mode, check return_response (second arg) to decide if client wants data
|
||||
// Use nested if constexpr to avoid compile error when tuple doesn't have enough elements
|
||||
// (std::tuple_element_t is evaluated before the && short-circuit, so we must nest)
|
||||
if constexpr (sizeof...(Ts) >= 2) {
|
||||
if constexpr (std::is_same_v<std::tuple_element_t<1, std::tuple<Ts...>>, bool>) {
|
||||
if (this->is_optional_mode_) {
|
||||
bool return_response = std::get<1>(args);
|
||||
if (!return_response) {
|
||||
// Client doesn't want response data, just send success/error
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, error_message,
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
}
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
TemplatableValue<bool, Ts...> success_{true};
|
||||
TemplatableValue<std::string, Ts...> error_message_{""};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
std::function<void(Ts..., JsonObject)> json_builder_;
|
||||
bool has_data_{false};
|
||||
#endif
|
||||
bool is_optional_mode_{false};
|
||||
};
|
||||
|
||||
// Action to unregister a service call after execution completes
|
||||
// Automatically appended to the end of action lists for non-none response modes
|
||||
template<typename... Ts> class APIUnregisterServiceCallAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
// Extract call_id from first argument - same convention as APIRespondAction
|
||||
auto args = std::make_tuple(x...);
|
||||
uint32_t call_id = std::get<0>(args);
|
||||
if (call_id != 0) {
|
||||
this->parent_->unregister_active_action_call(call_id);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -44,7 +44,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid(
|
||||
"The 'ble_client_id' option has been removed. Please migrate "
|
||||
"to the new `bedjet_id` option in the `bedjet` component.\n"
|
||||
"See https://esphome.io/components/climate/bedjet.html"
|
||||
"See https://esphome.io/components/climate/bedjet/"
|
||||
),
|
||||
cv.Optional(CONF_TIME_ID): cv.invalid(
|
||||
"The 'time_id' option has been moved to the `bedjet` component."
|
||||
|
||||
@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
|
||||
i2c::ErrorCode result_code =
|
||||
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
|
||||
if (result_code != i2c::ERROR_OK) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
@@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::cancel() {
|
||||
void MultiClickTrigger::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::trigger_() {
|
||||
void MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout("trigger");
|
||||
@@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
bool state;
|
||||
@@ -172,5 +171,4 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
@@ -70,6 +68,4 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
}
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
@@ -72,5 +70,4 @@ class BinarySensorInitiallyOff : public BinarySensor {
|
||||
bool has_state() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
#include "binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
@@ -132,6 +130,4 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
|
||||
@@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component {
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
#include "automation.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
class Automation {
|
||||
@@ -122,16 +121,19 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
std::vector<uint8_t> value;
|
||||
|
||||
bool result;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: copy from flash to vector
|
||||
value.assign(this->value_.data, this->value_.data + this->len_);
|
||||
// Static mode: write directly from flash pointer
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
} else {
|
||||
// Template mode: call function
|
||||
value = this->value_.func(x...);
|
||||
// Template mode: call function and write the vector
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value);
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!write(value))
|
||||
if (!result)
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
@@ -144,15 +146,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const std::vector<uint8_t> &value) {
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
@@ -160,6 +162,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
@@ -193,7 +197,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
ble_client_->address_str().c_str());
|
||||
ble_client_->address_str());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -386,7 +390,6 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_client";
|
||||
|
||||
@@ -39,7 +38,7 @@ void BLEClient::set_enabled(bool enabled) {
|
||||
return;
|
||||
this->enabled = enabled;
|
||||
if (!enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
@@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase {
|
||||
std::vector<BLEClientNode *> nodes_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_binary_output";
|
||||
|
||||
@@ -14,7 +13,7 @@ void BLEBinaryOutput::dump_config() {
|
||||
" MAC address : %s\n"
|
||||
" Service UUID : %s\n"
|
||||
" Characteristic UUID: %s",
|
||||
this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
LOG_BINARY_OUTPUT(this);
|
||||
}
|
||||
@@ -44,7 +43,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
this->parent()->address_str().c_str());
|
||||
this->parent()->address_str());
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
@@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) {
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
|
||||
esp_gatt_write_type_t write_type_{};
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
public:
|
||||
@@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
BLESensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_rssi_sensor";
|
||||
|
||||
@@ -19,7 +18,7 @@ void BLEClientRSSISensor::loop() {
|
||||
|
||||
void BLEClientRSSISensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str());
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
@@ -69,15 +68,14 @@ void BLEClientRSSISensor::update() {
|
||||
this->get_rssi_();
|
||||
}
|
||||
void BLEClientRSSISensor::get_rssi_() {
|
||||
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str());
|
||||
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str());
|
||||
auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda());
|
||||
if (status != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status);
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ
|
||||
bool should_update_{false};
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_sensor";
|
||||
|
||||
@@ -25,7 +24,7 @@ void BLESensor::dump_config() {
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -147,6 +146,5 @@ void BLESensor::update() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
||||
espbt::ESPBTUUID descr_uuid_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_switch";
|
||||
|
||||
@@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
|
||||
void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); }
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSensor {
|
||||
public:
|
||||
@@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSe
|
||||
BLETextSensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_text_sensor";
|
||||
|
||||
@@ -28,7 +27,7 @@ void BLETextSensor::dump_config() {
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -138,6 +137,5 @@ void BLETextSensor::update() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
|
||||
espbt::ESPBTUUID descr_uuid_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -87,17 +87,21 @@ void BLENUS::setup() {
|
||||
global_ble_nus = this;
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr && this->expose_log_) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
||||
const char c = '\n';
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
||||
});
|
||||
logger::global_logger->add_log_listener(this);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
(void) level;
|
||||
(void) tag;
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
||||
const char c = '\n';
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
void BLENUS::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ble nus:");
|
||||
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
|
||||
|
||||
@@ -2,12 +2,20 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#include <shell/shell_bt_nus.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace esphome::ble_nus {
|
||||
|
||||
class BLENUS : public Component {
|
||||
class BLENUS : public Component
|
||||
#ifdef USE_LOGGER
|
||||
,
|
||||
public logger::LogListener
|
||||
#endif
|
||||
{
|
||||
enum TxStatus {
|
||||
TX_DISABLED,
|
||||
TX_ENABLED,
|
||||
@@ -20,6 +28,9 @@ class BLENUS : public Component {
|
||||
void loop() override;
|
||||
size_t write_array(const uint8_t *data, size_t len);
|
||||
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
|
||||
#ifdef USE_LOGGER
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void send_enabled_callback(bt_nus_send_status status);
|
||||
|
||||
@@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str().c_str(),
|
||||
service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
|
||||
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
|
||||
service_status, service_count, this->send_service_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
@@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (resp.services.size() > 1) {
|
||||
resp.services.pop_back();
|
||||
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
|
||||
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
|
||||
this->connection_index_, this->address_str(), this->send_service_, current_size, service_size,
|
||||
MAX_PACKET_SIZE);
|
||||
// Don't increment send_service_ - we'll retry this service in next batch
|
||||
} else {
|
||||
// This single service is too large, but we have to send it anyway
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
|
||||
this->address_str().c_str(), this->send_service_, service_size);
|
||||
this->address_str(), this->send_service_, service_size);
|
||||
// Increment so we don't get stuck
|
||||
this->send_service_++;
|
||||
}
|
||||
@@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
|
||||
status);
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
|
||||
action, type);
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
|
||||
type);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
|
||||
operation, handle, status);
|
||||
}
|
||||
|
||||
@@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
|
||||
// This prevents race condition where we mark slot as free before controller cleanup is complete
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
|
||||
param->disconnect.reason);
|
||||
// Send disconnection notification but don't free the slot yet
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
|
||||
param->close.reason);
|
||||
// Now the GATT connection is fully closed and controller resources are freed
|
||||
// Safe to mark the connection slot as available
|
||||
@@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
|
||||
param->notify.handle);
|
||||
api::BluetoothGATTNotifyDataResponse resp;
|
||||
resp.address = this->address_;
|
||||
@@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
@@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
@@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
this->log_gatt_not_connected_("read", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
@@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *
|
||||
this->log_gatt_not_connected_("write", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
@@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
|
||||
|
||||
if (enable) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ void BluetoothProxy::setup() {
|
||||
// Capture the configured scan mode from YAML before any API changes
|
||||
this->configured_scan_active_ = this->parent_->get_scan_active();
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
this->parent_->add_scanner_state_listener(this);
|
||||
}
|
||||
|
||||
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
|
||||
@@ -47,12 +49,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
|
||||
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), espbt::client_state_to_string(state));
|
||||
connection->address_str(), espbt::client_state_to_string(state));
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
|
||||
message);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message);
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
|
||||
@@ -186,7 +187,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
}
|
||||
if (!msg.has_address_type) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
connection->address_str());
|
||||
this->send_device_connection(msg.address, false);
|
||||
return;
|
||||
}
|
||||
@@ -199,7 +200,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
connection->get_connection_index(), connection->address_str());
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -339,7 +340,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
|
||||
return;
|
||||
}
|
||||
if (!connection->service_count_) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str());
|
||||
this->send_gatt_services_done(msg.address);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
|
||||
};
|
||||
|
||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
public esp32_ble_tracker::BLEScannerStateListener,
|
||||
public Component {
|
||||
friend class BluetoothConnection; // Allow connection to update connections_free_response_
|
||||
public:
|
||||
BluetoothProxy();
|
||||
@@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
|
||||
void set_active(bool active) { this->active_ = active; }
|
||||
bool has_active() { return this->active_; }
|
||||
|
||||
/// BLEScannerStateListener interface
|
||||
void on_scanner_state(esp32_ble_tracker::ScannerState state) override;
|
||||
|
||||
uint32_t get_legacy_version() const {
|
||||
if (this->active_) {
|
||||
return LEGACY_ACTIVE_CONNECTIONS_VERSION;
|
||||
@@ -130,11 +135,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string get_bluetooth_mac_address_pretty() {
|
||||
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
|
||||
const uint8_t *mac = esp_bt_dev_get_address();
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
if (mac != nullptr) {
|
||||
format_mac_addr_upper(mac, output.data());
|
||||
} else {
|
||||
output[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
1
esphome/components/bm8563/__init__.py
Normal file
1
esphome/components/bm8563/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@abmantis"]
|
||||
198
esphome/components/bm8563/bm8563.cpp
Normal file
198
esphome/components/bm8563/bm8563.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "bm8563.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bm8563 {
|
||||
|
||||
static const char *const TAG = "bm8563";
|
||||
|
||||
static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00;
|
||||
static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01;
|
||||
static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4
|
||||
static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8
|
||||
static constexpr uint8_t TIMER_CONTROL_REG = 0x0E;
|
||||
static constexpr uint8_t TIMER_VALUE_REG = 0x0F;
|
||||
static constexpr uint8_t CLOCK_1_HZ = 0x82;
|
||||
static constexpr uint8_t CLOCK_1_60_HZ = 0x83;
|
||||
// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds
|
||||
static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
|
||||
|
||||
void BM8563::setup() {
|
||||
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::update() { this->read_time(); }
|
||||
|
||||
void BM8563::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BM8563:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::start_timer(uint32_t duration_s) {
|
||||
this->clear_irq_();
|
||||
this->set_timer_irq_(duration_s);
|
||||
}
|
||||
|
||||
void BM8563::write_time() {
|
||||
auto now = time::RealTimeClock::utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour,
|
||||
now.minute, now.second);
|
||||
|
||||
this->set_time_(now);
|
||||
this->set_date_(now);
|
||||
}
|
||||
|
||||
void BM8563::read_time() {
|
||||
ESPTime rtc_time;
|
||||
this->get_time_(rtc_time);
|
||||
this->get_date_(rtc_time);
|
||||
rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid
|
||||
ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
uint8_t BM8563::bcd2_to_byte_(uint8_t value) {
|
||||
const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10;
|
||||
return tmp + (value & 0x0F);
|
||||
}
|
||||
|
||||
uint8_t BM8563::byte_to_bcd2_(uint8_t value) {
|
||||
const uint8_t bcdhigh = value / 10;
|
||||
value -= bcdhigh * 10;
|
||||
return (bcdhigh << 4) | value;
|
||||
}
|
||||
|
||||
void BM8563::get_time_(ESPTime &time) {
|
||||
uint8_t buf[3] = {0};
|
||||
this->read_register(TIME_FIRST_REG, buf, 3);
|
||||
|
||||
time.second = this->bcd2_to_byte_(buf[0] & 0x7f);
|
||||
time.minute = this->bcd2_to_byte_(buf[1] & 0x7f);
|
||||
time.hour = this->bcd2_to_byte_(buf[2] & 0x3f);
|
||||
}
|
||||
|
||||
void BM8563::set_time_(const ESPTime &time) {
|
||||
uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)};
|
||||
this->write_register_(TIME_FIRST_REG, buf, 3);
|
||||
}
|
||||
|
||||
void BM8563::get_date_(ESPTime &time) {
|
||||
uint8_t buf[4] = {0};
|
||||
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
|
||||
|
||||
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
|
||||
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
|
||||
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
|
||||
|
||||
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
|
||||
|
||||
if (buf[2] & 0x80) {
|
||||
time.year = 1900 + year_byte;
|
||||
} else {
|
||||
time.year = 2000 + year_byte;
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
uint8_t buf[4] = {
|
||||
this->byte_to_bcd2_(time.day_of_month),
|
||||
this->byte_to_bcd2_(time.day_of_week),
|
||||
this->byte_to_bcd2_(time.month),
|
||||
this->byte_to_bcd2_(time.year % 100),
|
||||
};
|
||||
|
||||
if (time.year < 2000) {
|
||||
buf[2] = buf[2] | 0x80;
|
||||
}
|
||||
|
||||
this->write_register_(DATE_FIRST_REG, buf, 4);
|
||||
}
|
||||
|
||||
void BM8563::write_byte_(uint8_t reg, uint8_t value) {
|
||||
if (!this->write_byte(reg, value)) {
|
||||
ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value);
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) {
|
||||
if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len);
|
||||
}
|
||||
}
|
||||
|
||||
optional<uint8_t> BM8563::read_register_(uint8_t reg) {
|
||||
uint8_t data;
|
||||
if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to read register 0x%02X", reg);
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void BM8563::set_timer_irq_(uint32_t duration_s) {
|
||||
ESP_LOGI(TAG, "Timer Duration: %u s", duration_s);
|
||||
|
||||
if (duration_s > MAX_TIMER_DURATION_S) {
|
||||
ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S);
|
||||
return;
|
||||
}
|
||||
|
||||
if (duration_s > 255) {
|
||||
uint8_t duration_minutes = duration_s / 60;
|
||||
this->write_byte_(TIMER_VALUE_REG, duration_minutes);
|
||||
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ);
|
||||
} else {
|
||||
this->write_byte_(TIMER_VALUE_REG, duration_s);
|
||||
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ);
|
||||
}
|
||||
|
||||
auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_ctrl_status_2.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value();
|
||||
ctrl_status_2_reg_value |= (1 << 0);
|
||||
ctrl_status_2_reg_value &= ~(1 << 7);
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value);
|
||||
}
|
||||
|
||||
void BM8563::clear_irq_() {
|
||||
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_data.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t data = maybe_data.value();
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3);
|
||||
}
|
||||
|
||||
void BM8563::disable_irq_() {
|
||||
this->clear_irq_();
|
||||
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_data.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t data = maybe_data.value();
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc);
|
||||
}
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
57
esphome/components/bm8563/bm8563.h
Normal file
57
esphome/components/bm8563/bm8563.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::bm8563 {
|
||||
|
||||
class BM8563 : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void write_time();
|
||||
void read_time();
|
||||
void start_timer(uint32_t duration_s);
|
||||
|
||||
private:
|
||||
void get_time_(ESPTime &time);
|
||||
void get_date_(ESPTime &time);
|
||||
|
||||
void set_time_(const ESPTime &time);
|
||||
void set_date_(const ESPTime &time);
|
||||
|
||||
void set_timer_irq_(uint32_t duration_s);
|
||||
void clear_irq_();
|
||||
void disable_irq_();
|
||||
|
||||
void write_byte_(uint8_t reg, uint8_t value);
|
||||
void write_register_(uint8_t reg, const uint8_t *data, size_t len);
|
||||
optional<uint8_t> read_register_(uint8_t reg);
|
||||
|
||||
uint8_t bcd2_to_byte_(uint8_t value);
|
||||
uint8_t byte_to_bcd2_(uint8_t value);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class TimerAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto duration = this->duration_.value(x...);
|
||||
this->parent_->start_timer(duration);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
80
esphome/components/bm8563/time.py
Normal file
80
esphome/components/bm8563/time.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
I2C_ADDR = 0x51
|
||||
|
||||
bm8563_ns = cg.esphome_ns.namespace("bm8563")
|
||||
BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = bm8563_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = bm8563_ns.class_("ReadAction", automation.Action)
|
||||
TimerAction = bm8563_ns.class_("TimerAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BM8563),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(I2C_ADDR))
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.write_time",
|
||||
WriteAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_write_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.start_timer",
|
||||
TimerAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_start_timer_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_read_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
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)
|
||||
await time.register_time(var, config)
|
||||
@@ -100,18 +100,18 @@ void BME280Component::setup() {
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x60) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -120,12 +120,12 @@ void BME280Component::setup() {
|
||||
do { // NOLINT
|
||||
delay(2);
|
||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BME280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,26 +153,26 @@ void BME280Component::setup() {
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
|
||||
this->mark_failed("Read humidity control");
|
||||
this->mark_failed(LOG_STR("Read humidity control"));
|
||||
return;
|
||||
}
|
||||
humid_control_val &= ~0b00000111;
|
||||
humid_control_val |= this->humidity_oversampling_ & 0b111;
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
|
||||
this->mark_failed("Write humidity control");
|
||||
this->mark_failed(LOG_STR("Write humidity control"));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b101 << 5; // 1000 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.only_on_esp8266,
|
||||
cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(supported=[esp32.const.VARIANT_ESP32]),
|
||||
esp32.only_on_variant(supported=[esp32.VARIANT_ESP32]),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_,
|
||||
this->bme68x_status_);
|
||||
if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) {
|
||||
ESP_LOGE(TAG, "No sensors, add at least one sensor to the config");
|
||||
}
|
||||
}
|
||||
|
||||
if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) {
|
||||
|
||||
@@ -65,23 +65,23 @@ void BMP280Component::setup() {
|
||||
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x58) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -90,12 +90,12 @@ void BMP280Component::setup() {
|
||||
do {
|
||||
delay(2);
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BMP280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b000 << 5; // 0.5 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
namespace esphome::button {
|
||||
|
||||
template<typename... Ts> class PressAction : public Action<Ts...> {
|
||||
public:
|
||||
@@ -24,5 +23,4 @@ class ButtonPressTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace button
|
||||
} // namespace esphome
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "button.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
namespace esphome::button {
|
||||
|
||||
static const char *const TAG = "button";
|
||||
|
||||
@@ -26,5 +25,4 @@ void Button::press() {
|
||||
}
|
||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
|
||||
|
||||
} // namespace button
|
||||
} // namespace esphome
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
namespace esphome::button {
|
||||
|
||||
class Button;
|
||||
void log_button(const char *tag, const char *prefix, const char *type, Button *obj);
|
||||
@@ -45,5 +44,4 @@ class Button : public EntityBase, public EntityBase_DeviceClass {
|
||||
CallbackManager<void()> press_callback_{};
|
||||
};
|
||||
|
||||
} // namespace button
|
||||
} // namespace esphome
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -8,7 +8,7 @@ Camera *Camera::global_camera = nullptr;
|
||||
|
||||
Camera::Camera() {
|
||||
if (global_camera != nullptr) {
|
||||
this->status_set_error("Multiple cameras are configured, but only one is supported.");
|
||||
this->status_set_error(LOG_STR("Multiple cameras are configured, but only one is supported."));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ inline const char *to_string(PixelFormat format) {
|
||||
return "PIXEL_FORMAT_UNKNOWN";
|
||||
}
|
||||
|
||||
// Forward declaration
|
||||
class CameraImage;
|
||||
|
||||
/** Listener interface for camera events.
|
||||
*
|
||||
* Components can implement this interface to receive camera notifications
|
||||
* (new images, stream start/stop) without the overhead of std::function callbacks.
|
||||
*/
|
||||
class CameraListener {
|
||||
public:
|
||||
virtual void on_camera_image(const std::shared_ptr<CameraImage> &image) {}
|
||||
virtual void on_stream_start() {}
|
||||
virtual void on_stream_stop() {}
|
||||
};
|
||||
|
||||
/** Abstract camera image base class.
|
||||
* Encapsulates the JPEG encoded data and it is shared among
|
||||
* all connected clients.
|
||||
@@ -87,12 +102,12 @@ struct CameraImageSpec {
|
||||
};
|
||||
|
||||
/** Abstract camera base class. Collaborates with API.
|
||||
* 1) API server starts and installs callback (add_image_callback)
|
||||
* which is called by the camera when a new image is available.
|
||||
* 1) API server starts and registers as a listener (add_listener)
|
||||
* to receive new images from the camera.
|
||||
* 2) New API client connects and creates a new image reader (create_image_reader).
|
||||
* 3) API connection receives protobuf CameraImageRequest and calls request_image.
|
||||
* 3.a) API connection receives protobuf CameraImageRequest and calls start_stream.
|
||||
* 4) Camera implementation provides JPEG data in the CameraImage and calls callback.
|
||||
* 4) Camera implementation provides JPEG data in the CameraImage and notifies listeners.
|
||||
* 5) API connection sets the image in the image reader.
|
||||
* 6) API connection consumes data from the image reader and returns the image when finished.
|
||||
* 7.a) Camera captures a new image and continues with 4) until start_stream is called.
|
||||
@@ -100,8 +115,8 @@ struct CameraImageSpec {
|
||||
class Camera : public EntityBase, public Component {
|
||||
public:
|
||||
Camera();
|
||||
// Camera implementation invokes callback to publish a new image.
|
||||
virtual void add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&callback) = 0;
|
||||
/// Add a listener to receive camera events
|
||||
virtual void add_listener(CameraListener *listener) = 0;
|
||||
/// Returns a new camera image reader that keeps track of the JPEG data in the camera image.
|
||||
virtual CameraImageReader *create_image_reader() = 0;
|
||||
// Connection, camera or web server requests one new JPEG image.
|
||||
|
||||
@@ -13,14 +13,16 @@ static const char *const TAG = "captive_portal";
|
||||
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
|
||||
stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
|
||||
char mac_s[18];
|
||||
const char *mac_str = get_mac_address_pretty_into_buffer(mac_s);
|
||||
#ifdef USE_ESP8266
|
||||
stream->print(ESPHOME_F("{\"mac\":\""));
|
||||
stream->print(get_mac_address_pretty().c_str());
|
||||
stream->print(mac_str);
|
||||
stream->print(ESPHOME_F("\",\"name\":\""));
|
||||
stream->print(App.get_name().c_str());
|
||||
stream->print(ESPHOME_F("\",\"aps\":[{}"));
|
||||
#else
|
||||
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
|
||||
stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", mac_str, App.get_name().c_str());
|
||||
#endif
|
||||
|
||||
for (auto &scan : wifi::global_wifi_component->get_scan_result()) {
|
||||
@@ -63,12 +65,6 @@ void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
if (!this->initialized_) {
|
||||
this->base_->add_handler(this);
|
||||
#ifdef USE_ESP32
|
||||
// Enable LRU socket purging to handle captive portal detection probe bursts
|
||||
// OS captive portal detection makes many simultaneous HTTP requests which can
|
||||
// exhaust sockets. LRU purging automatically closes oldest idle connections.
|
||||
this->base_->get_server()->set_lru_purge_enable(true);
|
||||
#endif
|
||||
}
|
||||
|
||||
network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip();
|
||||
|
||||
@@ -40,10 +40,6 @@ class CaptivePortal : public AsyncWebHandler, public Component {
|
||||
void end() {
|
||||
this->active_ = false;
|
||||
this->disable_loop(); // Stop processing DNS requests
|
||||
#ifdef USE_ESP32
|
||||
// Disable LRU socket purging now that captive portal is done
|
||||
this->base_->get_server()->set_lru_purge_enable(false);
|
||||
#endif
|
||||
this->base_->deinit();
|
||||
if (this->dns_server_ != nullptr) {
|
||||
this->dns_server_->stop();
|
||||
|
||||
311
esphome/components/cc1101/__init__.py
Normal file
311
esphome/components/cc1101/__init__.py
Normal file
@@ -0,0 +1,311 @@
|
||||
from esphome import automation, pins
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
from esphome.components.const import CONF_CRC_ENABLE, CONF_ON_PACKET
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA,
|
||||
CONF_FREQUENCY,
|
||||
CONF_ID,
|
||||
CONF_WAIT_TIME,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
CODEOWNERS = ["@lygris", "@gabest11"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
MULTI_CONF = True
|
||||
|
||||
ns = cg.esphome_ns.namespace("cc1101")
|
||||
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
|
||||
|
||||
# Config keys
|
||||
CONF_OUTPUT_POWER = "output_power"
|
||||
CONF_RX_ATTENUATION = "rx_attenuation"
|
||||
CONF_DC_BLOCKING_FILTER = "dc_blocking_filter"
|
||||
CONF_IF_FREQUENCY = "if_frequency"
|
||||
CONF_FILTER_BANDWIDTH = "filter_bandwidth"
|
||||
CONF_CHANNEL_SPACING = "channel_spacing"
|
||||
CONF_FSK_DEVIATION = "fsk_deviation"
|
||||
CONF_MSK_DEVIATION = "msk_deviation"
|
||||
CONF_SYMBOL_RATE = "symbol_rate"
|
||||
CONF_SYNC_MODE = "sync_mode"
|
||||
CONF_CARRIER_SENSE_ABOVE_THRESHOLD = "carrier_sense_above_threshold"
|
||||
CONF_MODULATION_TYPE = "modulation_type"
|
||||
CONF_MANCHESTER = "manchester"
|
||||
CONF_NUM_PREAMBLE = "num_preamble"
|
||||
CONF_SYNC1 = "sync1"
|
||||
CONF_SYNC0 = "sync0"
|
||||
CONF_MAGN_TARGET = "magn_target"
|
||||
CONF_MAX_LNA_GAIN = "max_lna_gain"
|
||||
CONF_MAX_DVGA_GAIN = "max_dvga_gain"
|
||||
CONF_CARRIER_SENSE_ABS_THR = "carrier_sense_abs_thr"
|
||||
CONF_CARRIER_SENSE_REL_THR = "carrier_sense_rel_thr"
|
||||
CONF_LNA_PRIORITY = "lna_priority"
|
||||
CONF_FILTER_LENGTH_FSK_MSK = "filter_length_fsk_msk"
|
||||
CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook"
|
||||
CONF_FREEZE = "freeze"
|
||||
CONF_HYST_LEVEL = "hyst_level"
|
||||
|
||||
# Packet mode config keys
|
||||
CONF_PACKET_MODE = "packet_mode"
|
||||
CONF_PACKET_LENGTH = "packet_length"
|
||||
CONF_WHITENING = "whitening"
|
||||
CONF_GDO0_PIN = "gdo0_pin"
|
||||
|
||||
# Enums
|
||||
SyncMode = ns.enum("SyncMode", True)
|
||||
SYNC_MODE = {
|
||||
"None": SyncMode.SYNC_MODE_NONE,
|
||||
"15/16": SyncMode.SYNC_MODE_15_16,
|
||||
"16/16": SyncMode.SYNC_MODE_16_16,
|
||||
"30/32": SyncMode.SYNC_MODE_30_32,
|
||||
}
|
||||
|
||||
Modulation = ns.enum("Modulation", True)
|
||||
MODULATION = {
|
||||
"2-FSK": Modulation.MODULATION_2_FSK,
|
||||
"GFSK": Modulation.MODULATION_GFSK,
|
||||
"ASK/OOK": Modulation.MODULATION_ASK_OOK,
|
||||
"4-FSK": Modulation.MODULATION_4_FSK,
|
||||
"MSK": Modulation.MODULATION_MSK,
|
||||
}
|
||||
|
||||
RxAttenuation = ns.enum("RxAttenuation", True)
|
||||
RX_ATTENUATION = {
|
||||
"0dB": RxAttenuation.RX_ATTENUATION_0DB,
|
||||
"6dB": RxAttenuation.RX_ATTENUATION_6DB,
|
||||
"12dB": RxAttenuation.RX_ATTENUATION_12DB,
|
||||
"18dB": RxAttenuation.RX_ATTENUATION_18DB,
|
||||
}
|
||||
|
||||
MagnTarget = ns.enum("MagnTarget", True)
|
||||
MAGN_TARGET = {
|
||||
"24dB": MagnTarget.MAGN_TARGET_24DB,
|
||||
"27dB": MagnTarget.MAGN_TARGET_27DB,
|
||||
"30dB": MagnTarget.MAGN_TARGET_30DB,
|
||||
"33dB": MagnTarget.MAGN_TARGET_33DB,
|
||||
"36dB": MagnTarget.MAGN_TARGET_36DB,
|
||||
"38dB": MagnTarget.MAGN_TARGET_38DB,
|
||||
"40dB": MagnTarget.MAGN_TARGET_40DB,
|
||||
"42dB": MagnTarget.MAGN_TARGET_42DB,
|
||||
}
|
||||
|
||||
MaxLnaGain = ns.enum("MaxLnaGain", True)
|
||||
MAX_LNA_GAIN = {
|
||||
"Default": MaxLnaGain.MAX_LNA_GAIN_DEFAULT,
|
||||
"2.6dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_2P6DB,
|
||||
"6.1dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_6P1DB,
|
||||
"7.4dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_7P4DB,
|
||||
"9.2dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_9P2DB,
|
||||
"11.5dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_11P5DB,
|
||||
"14.6dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_14P6DB,
|
||||
"17.1dB": MaxLnaGain.MAX_LNA_GAIN_MINUS_17P1DB,
|
||||
}
|
||||
|
||||
MaxDvgaGain = ns.enum("MaxDvgaGain", True)
|
||||
MAX_DVGA_GAIN = {
|
||||
"Default": MaxDvgaGain.MAX_DVGA_GAIN_DEFAULT,
|
||||
"-1": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_1,
|
||||
"-2": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_2,
|
||||
"-3": MaxDvgaGain.MAX_DVGA_GAIN_MINUS_3,
|
||||
}
|
||||
|
||||
CarrierSenseRelThr = ns.enum("CarrierSenseRelThr", True)
|
||||
CARRIER_SENSE_REL_THR = {
|
||||
"Default": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_DEFAULT,
|
||||
"+6dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_6DB,
|
||||
"+10dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_10DB,
|
||||
"+14dB": CarrierSenseRelThr.CARRIER_SENSE_REL_THR_PLUS_14DB,
|
||||
}
|
||||
|
||||
FilterLengthFskMsk = ns.enum("FilterLengthFskMsk", True)
|
||||
FILTER_LENGTH_FSK_MSK = {
|
||||
"8": FilterLengthFskMsk.FILTER_LENGTH_8DB,
|
||||
"16": FilterLengthFskMsk.FILTER_LENGTH_16DB,
|
||||
"32": FilterLengthFskMsk.FILTER_LENGTH_32DB,
|
||||
"64": FilterLengthFskMsk.FILTER_LENGTH_64DB,
|
||||
}
|
||||
|
||||
FilterLengthAskOok = ns.enum("FilterLengthAskOok", True)
|
||||
FILTER_LENGTH_ASK_OOK = {
|
||||
"4dB": FilterLengthAskOok.FILTER_LENGTH_4DB,
|
||||
"8dB": FilterLengthAskOok.FILTER_LENGTH_8DB,
|
||||
"12dB": FilterLengthAskOok.FILTER_LENGTH_12DB,
|
||||
"16dB": FilterLengthAskOok.FILTER_LENGTH_16DB,
|
||||
}
|
||||
|
||||
Freeze = ns.enum("Freeze", True)
|
||||
FREEZE = {
|
||||
"Default": Freeze.FREEZE_DEFAULT,
|
||||
"On Sync": Freeze.FREEZE_ON_SYNC,
|
||||
"Analog Only": Freeze.FREEZE_ANALOG_ONLY,
|
||||
"Analog And Digital": Freeze.FREEZE_ANALOG_AND_DIGITAL,
|
||||
}
|
||||
|
||||
WaitTime = ns.enum("WaitTime", True)
|
||||
WAIT_TIME = {
|
||||
"8": WaitTime.WAIT_TIME_8_SAMPLES,
|
||||
"16": WaitTime.WAIT_TIME_16_SAMPLES,
|
||||
"24": WaitTime.WAIT_TIME_24_SAMPLES,
|
||||
"32": WaitTime.WAIT_TIME_32_SAMPLES,
|
||||
}
|
||||
|
||||
HystLevel = ns.enum("HystLevel", True)
|
||||
HYST_LEVEL = {
|
||||
"None": HystLevel.HYST_LEVEL_NONE,
|
||||
"Low": HystLevel.HYST_LEVEL_LOW,
|
||||
"Medium": HystLevel.HYST_LEVEL_MEDIUM,
|
||||
"High": HystLevel.HYST_LEVEL_HIGH,
|
||||
}
|
||||
|
||||
# Config key -> Validator mapping
|
||||
CONFIG_MAP = {
|
||||
CONF_OUTPUT_POWER: cv.float_range(min=-30.0, max=11.0),
|
||||
CONF_RX_ATTENUATION: cv.enum(RX_ATTENUATION, upper=False),
|
||||
CONF_DC_BLOCKING_FILTER: cv.boolean,
|
||||
CONF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=300000000, max=928000000)),
|
||||
CONF_IF_FREQUENCY: cv.All(cv.frequency, cv.float_range(min=25000, max=788000)),
|
||||
CONF_FILTER_BANDWIDTH: cv.All(cv.frequency, cv.float_range(min=58000, max=812000)),
|
||||
CONF_CHANNEL: cv.uint8_t,
|
||||
CONF_CHANNEL_SPACING: cv.All(cv.frequency, cv.float_range(min=25000, max=405000)),
|
||||
CONF_FSK_DEVIATION: cv.All(cv.frequency, cv.float_range(min=1500, max=381000)),
|
||||
CONF_MSK_DEVIATION: cv.int_range(min=1, max=8),
|
||||
CONF_SYMBOL_RATE: cv.float_range(min=600, max=500000),
|
||||
CONF_SYNC_MODE: cv.enum(SYNC_MODE, upper=False),
|
||||
CONF_CARRIER_SENSE_ABOVE_THRESHOLD: cv.boolean,
|
||||
CONF_MODULATION_TYPE: cv.enum(MODULATION, upper=False),
|
||||
CONF_MANCHESTER: cv.boolean,
|
||||
CONF_NUM_PREAMBLE: cv.int_range(min=0, max=7),
|
||||
CONF_SYNC1: cv.hex_uint8_t,
|
||||
CONF_SYNC0: cv.hex_uint8_t,
|
||||
CONF_MAGN_TARGET: cv.enum(MAGN_TARGET, upper=False),
|
||||
CONF_MAX_LNA_GAIN: cv.enum(MAX_LNA_GAIN, upper=False),
|
||||
CONF_MAX_DVGA_GAIN: cv.enum(MAX_DVGA_GAIN, upper=False),
|
||||
CONF_CARRIER_SENSE_ABS_THR: cv.int_range(min=-8, max=7),
|
||||
CONF_CARRIER_SENSE_REL_THR: cv.enum(CARRIER_SENSE_REL_THR, upper=False),
|
||||
CONF_LNA_PRIORITY: cv.boolean,
|
||||
CONF_FILTER_LENGTH_FSK_MSK: cv.enum(FILTER_LENGTH_FSK_MSK, upper=False),
|
||||
CONF_FILTER_LENGTH_ASK_OOK: cv.enum(FILTER_LENGTH_ASK_OOK, upper=False),
|
||||
CONF_FREEZE: cv.enum(FREEZE, upper=False),
|
||||
CONF_WAIT_TIME: cv.enum(WAIT_TIME, upper=False),
|
||||
CONF_HYST_LEVEL: cv.enum(HYST_LEVEL, upper=False),
|
||||
CONF_PACKET_MODE: cv.boolean,
|
||||
CONF_PACKET_LENGTH: cv.uint8_t,
|
||||
CONF_CRC_ENABLE: cv.boolean,
|
||||
CONF_WHITENING: cv.boolean,
|
||||
}
|
||||
|
||||
|
||||
def _validate_packet_mode(config):
|
||||
if config.get(CONF_PACKET_MODE, False):
|
||||
if CONF_GDO0_PIN not in config:
|
||||
raise cv.Invalid("gdo0_pin is required when packet_mode is enabled")
|
||||
if CONF_PACKET_LENGTH not in config:
|
||||
raise cv.Invalid("packet_length is required when packet_mode is enabled")
|
||||
if config[CONF_PACKET_LENGTH] > 64:
|
||||
raise cv.Invalid("packet_length must be <= 64 (FIFO size)")
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(CC1101Component),
|
||||
cv.Optional(CONF_GDO0_PIN): pins.internal_gpio_input_pin_schema,
|
||||
cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True),
|
||||
}
|
||||
)
|
||||
.extend({cv.Optional(key): validator for key, validator in CONFIG_MAP.items()})
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True)),
|
||||
_validate_packet_mode,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
for key in CONFIG_MAP:
|
||||
if key in config:
|
||||
cg.add(getattr(var, f"set_{key}")(config[key]))
|
||||
|
||||
if CONF_GDO0_PIN in config:
|
||||
gdo0_pin = await cg.gpio_pin_expression(config[CONF_GDO0_PIN])
|
||||
cg.add(var.set_gdo0_pin(gdo0_pin))
|
||||
if CONF_ON_PACKET in config:
|
||||
await automation.build_automation(
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
],
|
||||
config[CONF_ON_PACKET],
|
||||
)
|
||||
|
||||
|
||||
# Actions
|
||||
BeginTxAction = ns.class_("BeginTxAction", automation.Action)
|
||||
BeginRxAction = ns.class_("BeginRxAction", automation.Action)
|
||||
ResetAction = ns.class_("ResetAction", automation.Action)
|
||||
SetIdleAction = ns.class_("SetIdleAction", automation.Action)
|
||||
SendPacketAction = ns.class_(
|
||||
"SendPacketAction", automation.Action, cg.Parented.template(CC1101Component)
|
||||
)
|
||||
|
||||
CC1101_ACTION_SCHEMA = cv.Schema(
|
||||
maybe_simple_id({cv.GenerateID(CONF_ID): cv.use_id(CC1101Component)})
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("cc1101.begin_tx", BeginTxAction, CC1101_ACTION_SCHEMA)
|
||||
@automation.register_action("cc1101.begin_rx", BeginRxAction, CC1101_ACTION_SCHEMA)
|
||||
@automation.register_action("cc1101.reset", ResetAction, CC1101_ACTION_SCHEMA)
|
||||
@automation.register_action("cc1101.set_idle", SetIdleAction, CC1101_ACTION_SCHEMA)
|
||||
async def cc1101_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
return value.encode("utf-8")
|
||||
if isinstance(value, list):
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
raise cv.Invalid(
|
||||
"data must either be a string wrapped in quotes or a list of bytes"
|
||||
)
|
||||
|
||||
|
||||
SEND_PACKET_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(CC1101Component),
|
||||
cv.Required(CONF_DATA): cv.templatable(validate_raw_data),
|
||||
},
|
||||
key=CONF_DATA,
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"cc1101.send_packet", SendPacketAction, SEND_PACKET_ACTION_SCHEMA
|
||||
)
|
||||
async def send_packet_action_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
data = config[CONF_DATA]
|
||||
if isinstance(data, bytes):
|
||||
data = list(data)
|
||||
if cg.is_template(data):
|
||||
templ = await cg.templatable(data, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_data_template(templ))
|
||||
else:
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*data))
|
||||
cg.add(var.set_data_static(arr, len(data)))
|
||||
return var
|
||||
674
esphome/components/cc1101/cc1101.cpp
Normal file
674
esphome/components/cc1101/cc1101.cpp
Normal file
@@ -0,0 +1,674 @@
|
||||
#include "cc1101.h"
|
||||
#include "cc1101pa.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
static const char *const TAG = "cc1101";
|
||||
|
||||
static void split_float(float value, int mbits, uint8_t &e, uint32_t &m) {
|
||||
int e_tmp;
|
||||
float m_tmp = std::frexp(value, &e_tmp);
|
||||
if (e_tmp <= mbits) {
|
||||
e = 0;
|
||||
m = 0;
|
||||
return;
|
||||
}
|
||||
e = static_cast<uint8_t>(e_tmp - mbits - 1);
|
||||
m = static_cast<uint32_t>(((m_tmp * 2 - 1) * (1 << (mbits + 1))) + 1) >> 1;
|
||||
if (m == (1UL << mbits)) {
|
||||
e = e + 1;
|
||||
m = 0;
|
||||
}
|
||||
}
|
||||
|
||||
CC1101Component::CC1101Component() {
|
||||
// Datasheet defaults
|
||||
memset(&this->state_, 0, sizeof(this->state_));
|
||||
this->state_.GDO2_CFG = 0x0D; // Serial Data (for RX on GDO2)
|
||||
this->state_.GDO1_CFG = 0x2E;
|
||||
this->state_.GDO0_CFG = 0x0D; // Serial Data (for RX on GDO0 / TX Input)
|
||||
this->state_.FIFO_THR = 7;
|
||||
this->state_.SYNC1 = 0xD3;
|
||||
this->state_.SYNC0 = 0x91;
|
||||
this->state_.PKTLEN = 0xFF;
|
||||
this->state_.APPEND_STATUS = 1;
|
||||
this->state_.LENGTH_CONFIG = 1;
|
||||
this->state_.CRC_EN = 1;
|
||||
this->state_.WHITE_DATA = 1;
|
||||
this->state_.FREQ_IF = 0x0F;
|
||||
this->state_.FREQ2 = 0x1E;
|
||||
this->state_.FREQ1 = 0xC4;
|
||||
this->state_.FREQ0 = 0xEC;
|
||||
this->state_.DRATE_E = 0x0C;
|
||||
this->state_.CHANBW_E = 0x02;
|
||||
this->state_.DRATE_M = 0x22;
|
||||
this->state_.SYNC_MODE = 2;
|
||||
this->state_.CHANSPC_E = 2;
|
||||
this->state_.NUM_PREAMBLE = 2;
|
||||
this->state_.CHANSPC_M = 0xF8;
|
||||
this->state_.DEVIATION_M = 7;
|
||||
this->state_.DEVIATION_E = 4;
|
||||
this->state_.RX_TIME = 7;
|
||||
this->state_.CCA_MODE = 3;
|
||||
this->state_.PO_TIMEOUT = 1;
|
||||
this->state_.FOC_LIMIT = 2;
|
||||
this->state_.FOC_POST_K = 1;
|
||||
this->state_.FOC_PRE_K = 2;
|
||||
this->state_.FOC_BS_CS_GATE = 1;
|
||||
this->state_.BS_POST_KP = 1;
|
||||
this->state_.BS_POST_KI = 1;
|
||||
this->state_.BS_PRE_KP = 2;
|
||||
this->state_.BS_PRE_KI = 1;
|
||||
this->state_.MAGN_TARGET = 3;
|
||||
this->state_.AGC_LNA_PRIORITY = 1;
|
||||
this->state_.FILTER_LENGTH = 1;
|
||||
this->state_.WAIT_TIME = 1;
|
||||
this->state_.HYST_LEVEL = 2;
|
||||
this->state_.WOREVT1 = 0x87;
|
||||
this->state_.WOREVT0 = 0x6B;
|
||||
this->state_.RC_CAL = 1;
|
||||
this->state_.EVENT1 = 7;
|
||||
this->state_.RC_PD = 1;
|
||||
this->state_.MIX_CURRENT = 2;
|
||||
this->state_.LODIV_BUF_CURRENT_RX = 1;
|
||||
this->state_.LNA2MIX_CURRENT = 1;
|
||||
this->state_.LNA_CURRENT = 1;
|
||||
this->state_.LODIV_BUF_CURRENT_TX = 1;
|
||||
this->state_.FSCAL3_LO = 9;
|
||||
this->state_.CHP_CURR_CAL_EN = 2;
|
||||
this->state_.FSCAL3_HI = 2;
|
||||
this->state_.FSCAL2 = 0x0A;
|
||||
this->state_.FSCAL1 = 0x20;
|
||||
this->state_.FSCAL0 = 0x0D;
|
||||
this->state_.RCCTRL1 = 0x41;
|
||||
this->state_.FSTEST = 0x59;
|
||||
this->state_.PTEST = 0x7F;
|
||||
this->state_.AGCTEST = 0x3F;
|
||||
this->state_.TEST2 = 0x88;
|
||||
this->state_.TEST1 = 0x31;
|
||||
this->state_.TEST0_LO = 1;
|
||||
this->state_.VCO_SEL_CAL_EN = 1;
|
||||
this->state_.TEST0_HI = 2;
|
||||
|
||||
// PKTCTRL0
|
||||
this->state_.PKT_FORMAT = 3;
|
||||
this->state_.LENGTH_CONFIG = 2;
|
||||
this->state_.FS_AUTOCAL = 1;
|
||||
|
||||
// Default Settings
|
||||
this->set_frequency(433920);
|
||||
this->set_if_frequency(153);
|
||||
this->set_filter_bandwidth(203);
|
||||
this->set_channel(0);
|
||||
this->set_channel_spacing(200);
|
||||
this->set_symbol_rate(5000);
|
||||
this->set_sync_mode(SyncMode::SYNC_MODE_NONE);
|
||||
this->set_carrier_sense_above_threshold(true);
|
||||
this->set_modulation_type(Modulation::MODULATION_ASK_OOK);
|
||||
this->set_magn_target(MagnTarget::MAGN_TARGET_42DB);
|
||||
this->set_max_lna_gain(MaxLnaGain::MAX_LNA_GAIN_DEFAULT);
|
||||
this->set_max_dvga_gain(MaxDvgaGain::MAX_DVGA_GAIN_MINUS_3);
|
||||
this->set_lna_priority(false);
|
||||
this->set_wait_time(WaitTime::WAIT_TIME_32_SAMPLES);
|
||||
|
||||
// CRITICAL: Initialize PA Table to avoid transmitting 0 power (Silence)
|
||||
memset(this->pa_table_, 0, sizeof(this->pa_table_));
|
||||
this->set_output_power(10.0f);
|
||||
}
|
||||
|
||||
void CC1101Component::setup() {
|
||||
this->spi_setup();
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(false);
|
||||
delayMicroseconds(1);
|
||||
this->cs_->digital_write(true);
|
||||
delayMicroseconds(41);
|
||||
this->cs_->digital_write(false);
|
||||
delay(5);
|
||||
|
||||
this->strobe_(Command::RES);
|
||||
delay(5);
|
||||
|
||||
this->read_(Register::PARTNUM);
|
||||
this->read_(Register::VERSION);
|
||||
this->chip_id_ = encode_uint16(this->state_.PARTNUM, this->state_.VERSION);
|
||||
ESP_LOGD(TAG, "CC1101 found! Chip ID: 0x%04X", this->chip_id_);
|
||||
if (this->state_.VERSION == 0 || this->state_.PARTNUM == 0xFF) {
|
||||
ESP_LOGE(TAG, "Failed to verify CC1101.");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Setup GDO0 pin if configured
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->setup();
|
||||
}
|
||||
|
||||
this->initialized_ = true;
|
||||
|
||||
for (uint8_t i = 0; i <= static_cast<uint8_t>(Register::TEST0); i++) {
|
||||
if (i == static_cast<uint8_t>(Register::FSTEST) || i == static_cast<uint8_t>(Register::AGCTEST)) {
|
||||
continue;
|
||||
}
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->strobe_(Command::RX);
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->defer([this]() { this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT); });
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Read state
|
||||
this->read_(Register::RXBYTES);
|
||||
uint8_t rx_bytes = this->state_.NUM_RXBYTES;
|
||||
bool overflow = this->state_.RXFIFO_OVERFLOW;
|
||||
if (overflow || rx_bytes == 0) {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
|
||||
// Read packet
|
||||
uint8_t payload_length;
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->read_(Register::FIFO, &payload_length, 1);
|
||||
} else {
|
||||
payload_length = this->state_.PKTLEN;
|
||||
}
|
||||
if (payload_length == 0 || payload_length > 64) {
|
||||
ESP_LOGW(TAG, "Invalid payload length: %u", payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return;
|
||||
}
|
||||
this->packet_.resize(payload_length);
|
||||
this->read_(Register::FIFO, this->packet_.data(), payload_length);
|
||||
|
||||
// Read status and trigger
|
||||
uint8_t status[2];
|
||||
this->read_(Register::FIFO, status, 2);
|
||||
int8_t rssi_raw = static_cast<int8_t>(status[0]);
|
||||
float rssi = (rssi_raw * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (status[1] & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = status[1] & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
static const char *const MODULATION_NAMES[] = {"2-FSK", "GFSK", "UNUSED", "ASK/OOK",
|
||||
"4-FSK", "UNUSED", "UNUSED", "MSK"};
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
|
||||
ESP_LOGCONFIG(TAG, "CC1101:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Chip ID: 0x%04X\n"
|
||||
" Frequency: %" PRId32 " Hz\n"
|
||||
" Channel: %u\n"
|
||||
" Modulation: %s\n"
|
||||
" Symbol Rate: %.0f baud\n"
|
||||
" Filter Bandwidth: %.1f Hz\n"
|
||||
" Output Power: %.1f dBm",
|
||||
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
|
||||
symbol_rate, bw, this->output_power_effective_);
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
// Ensure Packet Format is 3 (Async Serial)
|
||||
this->write_(Register::PKTCTRL0, 0x32);
|
||||
ESP_LOGV(TAG, "Beginning TX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::TX, 50)) {
|
||||
ESP_LOGW(TAG, "Timed out waiting for TX state!");
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::begin_rx() {
|
||||
ESP_LOGV(TAG, "Beginning RX sequence");
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
|
||||
void CC1101Component::reset() {
|
||||
this->strobe_(Command::RES);
|
||||
this->setup();
|
||||
}
|
||||
|
||||
void CC1101Component::set_idle() {
|
||||
ESP_LOGV(TAG, "Setting IDLE state");
|
||||
this->enter_idle_();
|
||||
}
|
||||
|
||||
bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
|
||||
uint32_t start = millis();
|
||||
while (millis() - start < timeout_ms) {
|
||||
this->read_(Register::MARCSTATE);
|
||||
State s = static_cast<State>(this->state_.MARC_STATE);
|
||||
if (s == target_state) {
|
||||
return true;
|
||||
}
|
||||
delayMicroseconds(100);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CC1101Component::enter_idle_() {
|
||||
this->strobe_(Command::IDLE);
|
||||
this->wait_for_state_(State::IDLE);
|
||||
}
|
||||
|
||||
uint8_t CC1101Component::strobe_(Command cmd) {
|
||||
uint8_t index = static_cast<uint8_t>(cmd);
|
||||
if (cmd < Command::RES || cmd > Command::NOP) {
|
||||
return 0xFF;
|
||||
}
|
||||
this->enable();
|
||||
uint8_t status_byte = this->transfer_byte(index);
|
||||
this->disable();
|
||||
return status_byte;
|
||||
}
|
||||
|
||||
void CC1101Component::write_(Register reg) {
|
||||
uint8_t index = static_cast<uint8_t>(reg);
|
||||
this->enable();
|
||||
this->write_byte(index);
|
||||
this->write_array(&this->state_.regs()[index], 1);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void CC1101Component::write_(Register reg, uint8_t value) {
|
||||
uint8_t index = static_cast<uint8_t>(reg);
|
||||
this->state_.regs()[index] = value;
|
||||
this->write_(reg);
|
||||
}
|
||||
|
||||
void CC1101Component::write_(Register reg, const uint8_t *buffer, size_t length) {
|
||||
uint8_t index = static_cast<uint8_t>(reg);
|
||||
this->enable();
|
||||
this->write_byte(index | BUS_WRITE | BUS_BURST);
|
||||
this->write_array(buffer, length);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void CC1101Component::read_(Register reg) {
|
||||
uint8_t index = static_cast<uint8_t>(reg);
|
||||
this->enable();
|
||||
this->write_byte(index | BUS_READ | BUS_BURST);
|
||||
this->state_.regs()[index] = this->transfer_byte(0);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
void CC1101Component::read_(Register reg, uint8_t *buffer, size_t length) {
|
||||
uint8_t index = static_cast<uint8_t>(reg);
|
||||
this->enable();
|
||||
this->write_byte(index | BUS_READ | BUS_BURST);
|
||||
this->read_array(buffer, length);
|
||||
this->disable();
|
||||
}
|
||||
|
||||
CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet) {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO)) {
|
||||
return CC1101Error::PARAMS;
|
||||
}
|
||||
|
||||
// Write packet
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FTX);
|
||||
if (this->state_.LENGTH_CONFIG == static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE)) {
|
||||
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
|
||||
}
|
||||
this->write_(Register::FIFO, packet.data(), packet.size());
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::IDLE, 1000)) {
|
||||
ESP_LOGW(TAG, "TX timeout");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::TIMEOUT;
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
return CC1101Error::NONE;
|
||||
}
|
||||
|
||||
// Setters
|
||||
void CC1101Component::set_output_power(float value) {
|
||||
this->output_power_requested_ = value;
|
||||
int32_t freq = static_cast<int32_t>(this->state_.FREQ2 << 16 | this->state_.FREQ1 << 8 | this->state_.FREQ0) *
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
uint8_t a = 0xC0;
|
||||
if (freq >= 300000000 && freq <= 348000000) {
|
||||
a = PowerTableItem::find(PA_TABLE_315, sizeof(PA_TABLE_315) / sizeof(PA_TABLE_315[0]), value);
|
||||
} else if (freq >= 378000000 && freq <= 464000000) {
|
||||
a = PowerTableItem::find(PA_TABLE_433, sizeof(PA_TABLE_433) / sizeof(PA_TABLE_433[0]), value);
|
||||
} else if (freq >= 779000000 && freq < 900000000) {
|
||||
a = PowerTableItem::find(PA_TABLE_868, sizeof(PA_TABLE_868) / sizeof(PA_TABLE_868[0]), value);
|
||||
} else if (freq >= 900000000 && freq <= 928000000) {
|
||||
a = PowerTableItem::find(PA_TABLE_915, sizeof(PA_TABLE_915) / sizeof(PA_TABLE_915[0]), value);
|
||||
}
|
||||
|
||||
if (static_cast<Modulation>(this->state_.MOD_FORMAT) == Modulation::MODULATION_ASK_OOK) {
|
||||
this->pa_table_[0] = 0;
|
||||
this->pa_table_[1] = a;
|
||||
} else {
|
||||
this->pa_table_[0] = a;
|
||||
this->pa_table_[1] = 0;
|
||||
}
|
||||
this->output_power_effective_ = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PATABLE, this->pa_table_, sizeof(this->pa_table_));
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_rx_attenuation(RxAttenuation value) {
|
||||
this->state_.CLOSE_IN_RX = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_dc_blocking_filter(bool value) {
|
||||
this->state_.DEM_DCFILT_OFF = value ? 0 : 1;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_frequency(float value) {
|
||||
int32_t freq = static_cast<int32_t>(value * (1 << 16) / XTAL_FREQUENCY);
|
||||
this->state_.FREQ2 = static_cast<uint8_t>(freq >> 16);
|
||||
this->state_.FREQ1 = static_cast<uint8_t>(freq >> 8);
|
||||
this->state_.FREQ0 = static_cast<uint8_t>(freq);
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->write_(Register::FREQ2);
|
||||
this->write_(Register::FREQ1);
|
||||
this->write_(Register::FREQ0);
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_if_frequency(float value) {
|
||||
this->state_.FREQ_IF = value * (1 << 10) / XTAL_FREQUENCY;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FSCTRL1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_filter_bandwidth(float value) {
|
||||
uint8_t e;
|
||||
uint32_t m;
|
||||
split_float(XTAL_FREQUENCY / (value * 8), 2, e, m);
|
||||
this->state_.CHANBW_E = e;
|
||||
this->state_.CHANBW_M = static_cast<uint8_t>(m);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG4);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_channel(uint8_t value) {
|
||||
this->state_.CHANNR = value;
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->write_(Register::CHANNR);
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_channel_spacing(float value) {
|
||||
uint8_t e;
|
||||
uint32_t m;
|
||||
split_float(value * (1 << 18) / XTAL_FREQUENCY, 8, e, m);
|
||||
this->state_.CHANSPC_E = e;
|
||||
this->state_.CHANSPC_M = static_cast<uint8_t>(m);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG1);
|
||||
this->write_(Register::MDMCFG0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_fsk_deviation(float value) {
|
||||
uint8_t e;
|
||||
uint32_t m;
|
||||
split_float(value * (1 << 17) / XTAL_FREQUENCY, 3, e, m);
|
||||
this->state_.DEVIATION_E = e;
|
||||
this->state_.DEVIATION_M = static_cast<uint8_t>(m);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::DEVIATN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_msk_deviation(uint8_t value) {
|
||||
this->state_.DEVIATION_E = 0;
|
||||
this->state_.DEVIATION_M = value - 1;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::DEVIATN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_symbol_rate(float value) {
|
||||
uint8_t e;
|
||||
uint32_t m;
|
||||
split_float(value * (1 << 28) / XTAL_FREQUENCY, 8, e, m);
|
||||
this->state_.DRATE_E = e;
|
||||
this->state_.DRATE_M = static_cast<uint8_t>(m);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG4);
|
||||
this->write_(Register::MDMCFG3);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_sync_mode(SyncMode value) {
|
||||
this->state_.SYNC_MODE = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_carrier_sense_above_threshold(bool value) {
|
||||
this->state_.CARRIER_SENSE_ABOVE_THRESHOLD = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_modulation_type(Modulation value) {
|
||||
this->state_.MOD_FORMAT = static_cast<uint8_t>(value);
|
||||
this->state_.PA_POWER = value == Modulation::MODULATION_ASK_OOK ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::MDMCFG2);
|
||||
this->write_(Register::FREND0);
|
||||
this->strobe_(Command::RX);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_manchester(bool value) {
|
||||
this->state_.MANCHESTER_EN = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_num_preamble(uint8_t value) {
|
||||
this->state_.NUM_PREAMBLE = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::MDMCFG1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_sync1(uint8_t value) {
|
||||
this->state_.SYNC1 = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::SYNC1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_sync0(uint8_t value) {
|
||||
this->state_.SYNC0 = value;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::SYNC0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_magn_target(MagnTarget value) {
|
||||
this->state_.MAGN_TARGET = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_max_lna_gain(MaxLnaGain value) {
|
||||
this->state_.MAX_LNA_GAIN = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_max_dvga_gain(MaxDvgaGain value) {
|
||||
this->state_.MAX_DVGA_GAIN = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL2);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_carrier_sense_abs_thr(int8_t value) {
|
||||
this->state_.CARRIER_SENSE_ABS_THR = static_cast<uint8_t>(value & 0b1111);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_carrier_sense_rel_thr(CarrierSenseRelThr value) {
|
||||
this->state_.CARRIER_SENSE_REL_THR = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_lna_priority(bool value) {
|
||||
this->state_.AGC_LNA_PRIORITY = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL1);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_filter_length_fsk_msk(FilterLengthFskMsk value) {
|
||||
this->state_.FILTER_LENGTH = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_filter_length_ask_ook(FilterLengthAskOok value) {
|
||||
this->state_.FILTER_LENGTH = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_freeze(Freeze value) {
|
||||
this->state_.AGC_FREEZE = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_wait_time(WaitTime value) {
|
||||
this->state_.WAIT_TIME = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_hyst_level(HystLevel value) {
|
||||
this->state_.HYST_LEVEL = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::AGCCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
if (value) {
|
||||
// Configure GDO0 for FIFO status (asserts on RX FIFO threshold or end of packet)
|
||||
this->state_.GDO0_CFG = 0x01;
|
||||
// Set max RX FIFO threshold to ensure we only trigger on end-of-packet
|
||||
this->state_.FIFO_THR = 15;
|
||||
} else {
|
||||
// Configure GDO0 for serial data (async serial mode)
|
||||
this->state_.GDO0_CFG = 0x0D;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::IOCFG0);
|
||||
this->write_(Register::FIFOTHR);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_length(uint8_t value) {
|
||||
if (value == 0) {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_VARIABLE);
|
||||
} else {
|
||||
this->state_.LENGTH_CONFIG = static_cast<uint8_t>(LengthConfig::LENGTH_CONFIG_FIXED);
|
||||
this->state_.PKTLEN = value;
|
||||
}
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
this->write_(Register::PKTLEN);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_crc_enable(bool value) {
|
||||
this->state_.CRC_EN = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_whitening(bool value) {
|
||||
this->state_.WHITE_DATA = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::PKTCTRL0);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
153
esphome/components/cc1101/cc1101.h
Normal file
153
esphome/components/cc1101/cc1101.h
Normal file
@@ -0,0 +1,153 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/spi/spi.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "cc1101defs.h"
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
CC1101Component();
|
||||
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
// Actions
|
||||
void begin_tx();
|
||||
void begin_rx();
|
||||
void reset();
|
||||
void set_idle();
|
||||
|
||||
// GDO Pin Configuration
|
||||
void set_gdo0_pin(InternalGPIOPin *pin) { this->gdo0_pin_ = pin; }
|
||||
|
||||
// Configuration Setters
|
||||
void set_output_power(float value);
|
||||
void set_rx_attenuation(RxAttenuation value);
|
||||
void set_dc_blocking_filter(bool value);
|
||||
|
||||
// Tuner settings
|
||||
void set_frequency(float value);
|
||||
void set_if_frequency(float value);
|
||||
void set_filter_bandwidth(float value);
|
||||
void set_channel(uint8_t value);
|
||||
void set_channel_spacing(float value);
|
||||
void set_fsk_deviation(float value);
|
||||
void set_msk_deviation(uint8_t value);
|
||||
void set_symbol_rate(float value);
|
||||
void set_sync_mode(SyncMode value);
|
||||
void set_carrier_sense_above_threshold(bool value);
|
||||
void set_modulation_type(Modulation value);
|
||||
void set_manchester(bool value);
|
||||
void set_num_preamble(uint8_t value);
|
||||
void set_sync1(uint8_t value);
|
||||
void set_sync0(uint8_t value);
|
||||
|
||||
// AGC settings
|
||||
void set_magn_target(MagnTarget value);
|
||||
void set_max_lna_gain(MaxLnaGain value);
|
||||
void set_max_dvga_gain(MaxDvgaGain value);
|
||||
void set_carrier_sense_abs_thr(int8_t value);
|
||||
void set_carrier_sense_rel_thr(CarrierSenseRelThr value);
|
||||
void set_lna_priority(bool value);
|
||||
void set_filter_length_fsk_msk(FilterLengthFskMsk value);
|
||||
void set_filter_length_ask_ook(FilterLengthAskOok value);
|
||||
void set_freeze(Freeze value);
|
||||
void set_wait_time(WaitTime value);
|
||||
void set_hyst_level(HystLevel value);
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint8_t value);
|
||||
void set_crc_enable(bool value);
|
||||
void set_whitening(bool value);
|
||||
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
bool initialized_{false};
|
||||
|
||||
float output_power_requested_{10.0f};
|
||||
float output_power_effective_{10.0f};
|
||||
uint8_t pa_table_[PA_TABLE_SIZE]{};
|
||||
|
||||
CC1101State state_;
|
||||
|
||||
// GDO pin for packet reception
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
|
||||
// Packet handling
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
void write_(Register reg);
|
||||
void write_(Register reg, uint8_t value);
|
||||
void write_(Register reg, const uint8_t *buffer, size_t length);
|
||||
void read_(Register reg);
|
||||
void read_(Register reg, uint8_t *buffer, size_t length);
|
||||
|
||||
// State Management
|
||||
bool wait_for_state_(State target_state, uint32_t timeout_ms = 100);
|
||||
void enter_idle_();
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
template<typename... Ts> class BeginTxAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->begin_tx(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class BeginRxAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->begin_rx(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->reset(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetIdleAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->set_idle(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class SendPacketAction : public Action<Ts...>, public Parented<CC1101Component> {
|
||||
public:
|
||||
void set_data_template(std::function<std::vector<uint8_t>(Ts...)> func) { this->data_func_ = func; }
|
||||
void set_data_static(const uint8_t *data, size_t len) {
|
||||
this->data_static_ = data;
|
||||
this->data_static_len_ = len;
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
if (this->data_func_) {
|
||||
auto data = this->data_func_(x...);
|
||||
this->parent_->transmit_packet(data);
|
||||
} else if (this->data_static_ != nullptr) {
|
||||
std::vector<uint8_t> data(this->data_static_, this->data_static_ + this->data_static_len_);
|
||||
this->parent_->transmit_packet(data);
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::function<std::vector<uint8_t>(Ts...)> data_func_{};
|
||||
const uint8_t *data_static_{nullptr};
|
||||
size_t data_static_len_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::cc1101
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user