Compare commits

..

8 Commits

Author SHA1 Message Date
J. Nick Koston
4c9e4d30e9 tweak 2025-10-19 14:56:40 -10:00
J. Nick Koston
db42983f0c wip 2025-10-19 14:54:31 -10:00
J. Nick Koston
20c65f70ed wip 2025-10-19 14:47:19 -10:00
J. Nick Koston
38e31e328c wip 2025-10-19 14:46:25 -10:00
J. Nick Koston
58cecff778 wip 2025-10-19 14:44:04 -10:00
J. Nick Koston
1946656ea8 wip 2025-10-19 14:40:47 -10:00
J. Nick Koston
c9700a0450 wip 2025-10-19 14:35:09 -10:00
J. Nick Koston
0eab64ffe5 cache github downloads 2025-10-19 14:33:26 -10:00
2078 changed files with 17909 additions and 51587 deletions

View File

@@ -51,79 +51,7 @@ This document provides essential context for AI models interacting with this pro
* **Naming Conventions:** * **Naming Conventions:**
* **Python:** Follows PEP 8. Use clear, descriptive names following snake_case. * **Python:** Follows PEP 8. Use clear, descriptive names following snake_case.
* **C++:** Follows the Google C++ Style Guide with these specifics (following clang-tidy conventions): * **C++:** Follows the Google C++ Style Guide.
- Function, method, and variable names: `lower_snake_case`
- Class/struct/enum names: `UpperCamelCase`
- Top-level constants (global/namespace scope): `UPPER_SNAKE_CASE`
- Function-local constants: `lower_snake_case`
- Protected/private fields: `lower_snake_case_with_trailing_underscore_`
- Favor descriptive names over abbreviations
* **C++ Field Visibility:**
* **Prefer `protected`:** Use `protected` for most class fields to enable extensibility and testing. Fields should be `lower_snake_case_with_trailing_underscore_`.
* **Use `private` for safety-critical cases:** Use `private` visibility when direct field access could introduce bugs or violate invariants:
1. **Pointer lifetime issues:** When setters validate and store pointers from known lists to prevent dangling references.
```cpp
// Helper to find matching string in vector and return its pointer
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
class ClimateDevice {
public:
void set_custom_fan_modes(std::initializer_list<const char *> modes) {
this->custom_fan_modes_ = modes;
this->active_custom_fan_mode_ = nullptr; // Reset when modes change
}
bool set_custom_fan_mode(const char *mode) {
// Find mode in supported list and store that pointer (not the input pointer)
const char *validated_mode = vector_find(this->custom_fan_modes_, mode);
if (validated_mode != nullptr) {
this->active_custom_fan_mode_ = validated_mode;
return true;
}
return false;
}
private:
std::vector<const char *> custom_fan_modes_; // Pointers to string literals in flash
const char *active_custom_fan_mode_{nullptr}; // Must point to entry in custom_fan_modes_
};
```
2. **Invariant coupling:** When multiple fields must remain synchronized to prevent buffer overflows or data corruption.
```cpp
class Buffer {
public:
void resize(size_t new_size) {
auto new_data = std::make_unique<uint8_t[]>(new_size);
if (this->data_) {
std::memcpy(new_data.get(), this->data_.get(), std::min(this->size_, new_size));
}
this->data_ = std::move(new_data);
this->size_ = new_size; // Must stay in sync with data_
}
private:
std::unique_ptr<uint8_t[]> data_;
size_t size_{0}; // Must match allocated size of data_
};
```
3. **Resource management:** When setters perform cleanup or registration operations that derived classes might skip.
* **Provide `protected` accessor methods:** When derived classes need controlled access to `private` members.
* **C++ Preprocessor Directives:**
* **Avoid `#define` for constants:** Using `#define` for constants is discouraged and should be replaced with `const` variables or enums.
* **Use `#define` only for:**
- Conditional compilation (`#ifdef`, `#ifndef`)
- Compile-time sizes calculated during Python code generation (e.g., configuring `std::array` or `StaticVector` dimensions via `cg.add_define()`)
* **C++ Additional Conventions:**
* **Member access:** Prefix all class member access with `this->` (e.g., `this->value_` not `value_`)
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Component Structure:** * **Component Structure:**
* **Standard Files:** * **Standard Files:**
@@ -172,7 +100,8 @@ This document provides essential context for AI models interacting with this pro
* **C++ Class Pattern:** * **C++ Class Pattern:**
```cpp ```cpp
namespace esphome::my_component { namespace esphome {
namespace my_component {
class MyComponent : public Component { class MyComponent : public Component {
public: public:
@@ -188,7 +117,8 @@ This document provides essential context for AI models interacting with this pro
int param_{0}; int param_{0};
}; };
} // namespace esphome::my_component } // namespace my_component
} // namespace esphome
``` ```
* **Common Component Examples:** * **Common Component Examples:**
@@ -276,12 +206,12 @@ This document provides essential context for AI models interacting with this pro
## 7. Specific Instructions for AI Collaboration ## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):** * **Contribution Workflow (Pull Request Process):**
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). 1. **Fork & Branch:** Create a new branch in your fork.
2. **Make Changes:** Adhere to all coding conventions and patterns. 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. 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. 4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages. 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 using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template. 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.
* **Documentation Contributions:** * **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository. * Documentation is hosted in the separate `esphome/esphome-docs` repository.
@@ -402,45 +332,35 @@ This document provides essential context for AI models interacting with this pro
_use_feature = True _use_feature = True
``` ```
**Bad Pattern (Flat Keys):** **Good Pattern (CORE.data with Helpers):**
```python ```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 from esphome.core import CORE
DOMAIN = "my_component" # Keys for CORE.data storage
COMPONENT_STATE_KEY = "my_component_state"
USE_FEATURE_KEY = "my_component_use_feature"
@dataclass def _get_component_state() -> list:
class MyComponentData: """Get component state from CORE.data."""
feature_enabled: bool = False return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_data() -> MyComponentData: def _get_use_feature() -> bool | None:
if DOMAIN not in CORE.data: """Get feature flag from CORE.data."""
CORE.data[DOMAIN] = MyComponentData() return CORE.data.get(USE_FEATURE_KEY)
return CORE.data[DOMAIN]
def request_feature() -> None: def _set_use_feature(value: bool) -> None:
_get_data().feature_enabled = True """Set feature flag in CORE.data."""
CORE.data[USE_FEATURE_KEY] = value
def add_item(item: str) -> None: def enable_feature():
_get_data().items.append(item) _set_use_feature(True)
``` ```
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:** **Why this matters:**
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec - Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
- `CORE.data` automatically clears between runs - `CORE.data` automatically clears between runs
- Namespacing under `DOMAIN` prevents key collisions between components - Typed helper functions provide better IDE support and maintainability
- `@dataclass` provides type safety and cleaner attribute access - Encapsulation makes state management explicit and testable
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys. * **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.
@@ -448,45 +368,3 @@ This document provides essential context for AI models interacting with this pro
* **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`. * **Python:** When adding a new Python dependency, add it to the appropriate `requirements*.txt` file and `pyproject.toml`.
* **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`. * **C++ / PlatformIO:** When adding a new C++ dependency, add it to `platformio.ini` and use `cg.add_library`.
* **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags. * **Build Flags:** Use `cg.add_build_flag(...)` to add compiler flags.
## 8. Public API and Breaking Changes
* **Public C++ API:**
* **Components**: Only documented features at [esphome.io](https://esphome.io) are public API. Undocumented `public` members are internal.
* **Core/Base Classes** (`esphome/core/`, `Component`, `Sensor`, etc.): All `public` members are public API.
* **Components with Global Accessors** (`global_api_server`, etc.): All `public` members are public API (except config setters).
* **Public Python API:**
* All documented configuration options at [esphome.io](https://esphome.io) are public API.
* Python code in `esphome/core/` actively used by existing core components is considered stable API.
* Other Python code is internal unless explicitly documented for external component use.
* **Breaking Changes Policy:**
* Aim for **6-month deprecation window** when possible
* Clean breaks allowed for: signature changes, deep refactorings, resource constraints
* Must document migration path in PR description (generates release notes)
* Blog post required for core/base class changes or significant architectural changes
* Full details: https://developers.esphome.io/contributing/code/#public-api-and-breaking-changes
* **Breaking Change Checklist:**
- [ ] Clear justification (RAM/flash savings, architectural improvement)
- [ ] Explored non-breaking alternatives
- [ ] Added deprecation warnings if possible (use `ESPDEPRECATED` macro for C++)
- [ ] Documented migration path in PR description with before/after examples
- [ ] Updated all internal usage and esphome-docs
- [ ] Tested backward compatibility during deprecation period
* **Deprecation Pattern (C++):**
```cpp
// Remove before 2026.6.0
ESPDEPRECATED("Use new_method() instead. Removed in 2026.6.0", "2025.12.0")
void old_method() { this->new_method(); }
```
* **Deprecation Pattern (Python):**
```python
# Remove before 2026.6.0
if CONF_OLD_KEY in config:
_LOGGER.warning(f"'{CONF_OLD_KEY}' deprecated, use '{CONF_NEW_KEY}'. Removed in 2026.6.0")
config[CONF_NEW_KEY] = config.pop(CONF_OLD_KEY) # Auto-migrate
```

View File

@@ -1 +1 @@
4268ab0b5150f79ab1c317e8f3834c8bb0b4c8122da4f6b1fd67c49d0f2098c9 d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248

View File

@@ -7,7 +7,6 @@
- [ ] Bugfix (non-breaking change which fixes an issue) - [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality) - [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] 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 - [ ] Code quality improvements to existing code or addition of tests
- [ ] Other - [ ] Other

View File

@@ -17,12 +17,12 @@ runs:
steps: steps:
- name: Set up Python ${{ inputs.python-version }} - name: Set up Python ${{ inputs.python-version }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ inputs.python-version }} python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length

View File

@@ -22,11 +22,11 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot' if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate a token - name: Generate a token
id: generate-token id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2 uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
with: with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }} app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }} private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -53,7 +53,6 @@ jobs:
'new-target-platform', 'new-target-platform',
'merging-to-release', 'merging-to-release',
'merging-to-beta', 'merging-to-beta',
'chained-pr',
'core', 'core',
'small-pr', 'small-pr',
'dashboard', 'dashboard',
@@ -68,7 +67,6 @@ jobs:
'bugfix', 'bugfix',
'new-feature', 'new-feature',
'breaking-change', 'breaking-change',
'developer-breaking-change',
'code-quality' 'code-quality'
]; ];
@@ -142,8 +140,6 @@ jobs:
labels.add('merging-to-release'); labels.add('merging-to-release');
} else if (baseRef === 'beta') { } else if (baseRef === 'beta') {
labels.add('merging-to-beta'); labels.add('merging-to-beta');
} else if (baseRef !== 'dev') {
labels.add('chained-pr');
} }
return labels; return labels;
@@ -368,7 +364,6 @@ jobs:
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' }, { 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\] 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\] 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' } { pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
]; ];
@@ -418,7 +413,7 @@ jobs:
} }
// Generate review messages // Generate review messages
function generateReviewMessages(finalLabels, originalLabelCount) { function generateReviewMessages(finalLabels) {
const messages = []; const messages = [];
const prAuthor = context.payload.pull_request.user.login; const prAuthor = context.payload.pull_request.user.login;
@@ -432,15 +427,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = originalLabelCount > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD; const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`; let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) { if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
} else if (tooManyLabels) { } else if (tooManyLabels) {
message += `This PR affects ${originalLabelCount} different components/areas.`; message += `This PR affects ${finalLabels.length} different components/areas.`;
} else { } else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`; message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
} }
@@ -468,8 +463,8 @@ jobs:
} }
// Handle reviews // Handle reviews
async function handleReviews(finalLabels, originalLabelCount) { async function handleReviews(finalLabels) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount); const reviewMessages = generateReviewMessages(finalLabels);
const hasReviewableLabels = finalLabels.some(label => const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label) ['too-big', 'needs-codeowners'].includes(label)
); );
@@ -533,8 +528,8 @@ jobs:
const apiData = await fetchApiData(); const apiData = await fetchApiData();
const baseRef = context.payload.pull_request.base.ref; const baseRef = context.payload.pull_request.base.ref;
// Early exit for release and beta branches only // Early exit for non-dev branches
if (baseRef === 'release' || baseRef === 'beta') { if (baseRef !== 'dev') {
const branchLabels = await detectMergeBranch(); const branchLabels = await detectMergeBranch();
const finalLabels = Array.from(branchLabels); const finalLabels = Array.from(branchLabels);
@@ -629,7 +624,6 @@ jobs:
// Handle too many labels (only for non-mega PRs) // Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS; const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) { if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big']; finalLabels = ['too-big'];
@@ -638,7 +632,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', ')); console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews // Handle reviews
await handleReviews(finalLabels, originalLabelCount); await handleReviews(finalLabels);
// Apply labels // Apply labels
if (finalLabels.length > 0) { if (finalLabels.length > 0) {

View File

@@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
@@ -62,7 +62,7 @@ jobs:
run: git diff run: git diff
- if: failure() - if: failure()
name: Archive artifacts name: Archive artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: generated-proto-files name: generated-proto-files
path: | path: |

View File

@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"

View File

@@ -43,13 +43,13 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Set TAG - name: Set TAG
run: | run: |

View File

@@ -28,28 +28,25 @@ jobs:
run: | run: |
# Get PR details by searching for PR with matching head SHA # Get PR details by searching for PR with matching head SHA
# The workflow_run.pull_requests field is often empty for forks # The workflow_run.pull_requests field is often empty for forks
# Use paginate to handle repos with many open PRs
head_sha="${{ github.event.workflow_run.head_sha }}" head_sha="${{ github.event.workflow_run.head_sha }}"
pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \ pr_data=$(gh api "/repos/${{ github.repository }}/commits/$head_sha/pulls" \
--jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \ --jq '.[0] | {number: .number, base_ref: .base.ref}')
| head -n 1) if [ -z "$pr_data" ] || [ "$pr_data" == "null" ]; then
if [ -z "$pr_data" ]; then
echo "No PR found for SHA $head_sha, skipping" echo "No PR found for SHA $head_sha, skipping"
echo "skip=true" >> "$GITHUB_OUTPUT" echo "skip=true" >> $GITHUB_OUTPUT
exit 0 exit 0
fi fi
pr_number=$(echo "$pr_data" | jq -r '.number') pr_number=$(echo "$pr_data" | jq -r '.number')
base_ref=$(echo "$pr_data" | jq -r '.base_ref') base_ref=$(echo "$pr_data" | jq -r '.base_ref')
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT" echo "pr_number=$pr_number" >> $GITHUB_OUTPUT
echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT" echo "base_ref=$base_ref" >> $GITHUB_OUTPUT
echo "Found PR #$pr_number targeting base branch: $base_ref" echo "Found PR #$pr_number targeting base branch: $base_ref"
- name: Check out code from base repository - name: Check out code from base repository
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Always check out from the base repository (esphome/esphome), never from forks # 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 # Use the PR's target branch to ensure we run trusted code from the main repo
@@ -90,9 +87,9 @@ jobs:
if: steps.pr.outputs.skip != 'true' if: steps.pr.outputs.skip != 'true'
run: | run: |
if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
echo "found=true" >> "$GITHUB_OUTPUT" echo "found=true" >> $GITHUB_OUTPUT
else else
echo "found=false" >> "$GITHUB_OUTPUT" echo "found=false" >> $GITHUB_OUTPUT
echo "Memory analysis artifacts not found, skipping comment" echo "Memory analysis artifacts not found, skipping comment"
fi fi

View File

@@ -36,18 +36,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }} - name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true' if: needs.determine-jobs.outputs.python-linters == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -91,7 +91,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@@ -114,7 +114,7 @@ jobs:
matrix: matrix:
python-version: python-version:
- "3.11" - "3.11"
- "3.13" - "3.14"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@@ -123,16 +123,16 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.13" - python-version: "3.14"
os: windows-latest os: windows-latest
- python-version: "3.13" - python-version: "3.14"
os: macOS-latest os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
id: restore-python id: restore-python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
@@ -152,12 +152,12 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/ pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache - name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -170,20 +170,15 @@ jobs:
outputs: outputs:
integration-tests: ${{ steps.determine.outputs.integration-tests }} integration-tests: ${{ steps.determine.outputs.integration-tests }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }} clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }} python-linters: ${{ steps.determine.outputs.python-linters }}
changed-components: ${{ steps.determine.outputs.changed-components }} changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }} changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }} directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
component-test-count: ${{ steps.determine.outputs.component-test-count }} component-test-count: ${{ steps.determine.outputs.component-test-count }}
changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
memory_impact: ${{ steps.determine.outputs.memory-impact }} memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Fetch enough history to find the merge base # Fetch enough history to find the merge base
fetch-depth: 2 fetch-depth: 2
@@ -192,11 +187,6 @@ jobs:
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
- name: Determine which tests to run - name: Determine which tests to run
id: determine id: determine
env: env:
@@ -210,23 +200,12 @@ jobs:
# Extract individual fields # Extract individual fields
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
integration-tests: integration-tests:
name: Run integration tests name: Run integration tests
@@ -237,15 +216,15 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true' if: needs.determine-jobs.outputs.integration-tests == 'true'
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python 3.13 - name: Set up Python 3.13
id: python id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.13" python-version: "3.13"
- name: Restore Python virtual environment - name: Restore Python virtual environment
id: cache-venv id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: venv path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -264,34 +243,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --no-cov --tb=native -n auto tests/integration/ pytest -vv --no-cov --tb=native -n auto tests/integration/
cpp-unit-tests: clang-tidy:
name: Run C++ unit tests
runs-on: ubuntu-24.04
needs:
- common
- determine-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@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 }}
- name: Run cpp_unit_test.py
run: |
. venv/bin/activate
if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then
script/cpp_unit_test.py --all
else
ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs)
script/cpp_unit_test.py $ARGS
fi
clang-tidy-single:
name: ${{ matrix.name }} name: ${{ matrix.name }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
@@ -309,6 +261,22 @@ jobs:
name: Run script/clang-tidy for ESP8266 name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 options: --environment esp8266-arduino-tidy --grep USE_ESP8266
pio_cache_key: tidyesp8266 pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
pio_cache_key: tidyesp32
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
pio_cache_key: tidyesp32
- id: clang-tidy - id: clang-tidy
name: Run script/clang-tidy for ESP32 IDF name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF
@@ -321,7 +289,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# Need history for HEAD~1 to work for checking changed files # Need history for HEAD~1 to work for checking changed files
fetch-depth: 2 fetch-depth: 2
@@ -334,14 +302,14 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: github.ref == 'refs/heads/dev' if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio - name: Cache platformio
if: github.ref != 'refs/heads/dev' if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -389,165 +357,45 @@ jobs:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()
clang-tidy-nosplit: test-build-components-splitter:
name: Run script/clang-tidy for ESP32 Arduino name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit' if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
env: outputs:
GH_TOKEN: ${{ github.token }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
- name: Cache platformio id: split
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: | run: |
. venv/bin/activate . venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT # Use intelligent splitter that groups components with same bus configs
echo "reason=hash_changed" >> $GITHUB_OUTPUT components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else else
echo "full_scan=false" >> $GITHUB_OUTPUT directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "reason=normal" >> $GITHUB_OUTPUT echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi fi
- name: Run clang-tidy echo "Splitting components intelligently..."
run: | output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes echo "$output" >> $GITHUB_OUTPUT
run: script/ci-suggest-changes
if: always()
clang-tidy-split:
name: ${{ matrix.name }}
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
env:
GH_TOKEN: ${{ github.token }}
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 1/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 2/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 3/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino 4/4
options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Register problem matchers
run: |
echo "::add-matcher::.github/workflows/matchers/gcc.json"
echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
- name: Check if full clang-tidy scan needed
id: check_full_scan
run: |
. venv/bin/activate
if python script/clang_tidy_hash.py --check; then
echo "full_scan=true" >> $GITHUB_OUTPUT
echo "reason=hash_changed" >> $GITHUB_OUTPUT
else
echo "full_scan=false" >> $GITHUB_OUTPUT
echo "reason=normal" >> $GITHUB_OUTPUT
fi
- name: Run clang-tidy
run: |
. venv/bin/activate
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
echo "Running FULL clang-tidy scan (hash changed)"
script/clang-tidy --all-headers --fix ${{ matrix.options }}
else
echo "Running clang-tidy on changed files only"
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
fi
env:
# Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes
run: script/ci-suggest-changes
if: always()
test-build-components-split: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
@@ -555,12 +403,13 @@ jobs:
needs: needs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }} components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
steps: steps:
- name: Show disk space - name: Show disk space
run: | run: |
@@ -577,12 +426,27 @@ jobs:
version: 1.0 version: 1.0
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-test-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-test-${{ hashFiles('platformio.ini') }}
- name: Validate and compile components with intelligent grouping - name: Validate and compile components with intelligent grouping
run: | run: |
. venv/bin/activate . venv/bin/activate
@@ -662,13 +526,13 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env: env:
SKIP: pylint,clang-tidy-hash SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0 - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
@@ -688,7 +552,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }} skip: ${{ steps.check-script.outputs.skip }}
steps: steps:
- name: Check out target branch - name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: ${{ github.base_ref }} ref: ${{ github.base_ref }}
@@ -735,7 +599,7 @@ jobs:
- name: Restore cached memory analysis - name: Restore cached memory analysis
id: cache-memory-analysis id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +623,7 @@ jobs:
- name: Cache platformio - name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +664,7 @@ jobs:
- name: Save memory analysis to cache - name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: memory-analysis-target.json path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }} key: ${{ steps.cache-key.outputs.cache-key }}
@@ -821,7 +685,7 @@ jobs:
fi fi
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: memory-analysis-target name: memory-analysis-target
path: memory-analysis-target.json path: memory-analysis-target.json
@@ -840,14 +704,14 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }} flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps: steps:
- name: Check out PR branch - name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio - name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1 uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with: with:
path: ~/.platformio path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -885,7 +749,7 @@ jobs:
--platform "$platform" --platform "$platform"
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: memory-analysis-pr.json path: memory-analysis-pr.json
@@ -908,20 +772,20 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON - name: Download target analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: memory-analysis-target name: memory-analysis-target
path: ./memory-analysis path: ./memory-analysis
continue-on-error: true continue-on-error: true
- name: Download PR analysis JSON - name: Download PR analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: ./memory-analysis path: ./memory-analysis
@@ -948,10 +812,9 @@ jobs:
- pylint - pylint
- pytest - pytest
- integration-tests - integration-tests
- clang-tidy-single - clang-tidy
- clang-tidy-nosplit
- clang-tidy-split
- determine-jobs - determine-jobs
- test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite
- memory-impact-target-branch - memory-impact-target-branch
@@ -959,13 +822,13 @@ jobs:
- memory-impact-comment - memory-impact-comment
if: always() if: always()
steps: steps:
- name: Check job results - name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
env: env:
NEEDS_JSON: ${{ toJSON(needs) }} JSON_DOC: ${{ toJSON(needs) }}
run: | run: |
# memory-impact-target-branch is allowed to fail without blocking CI. echo $JSON_DOC | jq
# This job builds the target branch (dev/beta/release) which may fail because: exit 1
# 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")'

View File

@@ -21,7 +21,7 @@ permissions:
jobs: jobs:
request-codeowner-reviews: request-codeowner-reviews:
name: Run name: Run
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }} if: ${{ !github.event.pull_request.draft }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Request reviews from component codeowners - name: Request reviews from component codeowners

View File

@@ -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 # 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: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }} build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1 exit 1
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }} deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@@ -60,9 +60,9 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Build - name: Build
@@ -92,14 +92,14 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python - name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: "3.11" python-version: "3.11"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub - name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }} # version: ${{ needs.init.outputs.tag }}
- name: Upload digests - name: Upload digests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: digests-${{ matrix.platform.arch }} name: digests-${{ matrix.platform.arch }}
path: /tmp/digests path: /tmp/digests
@@ -168,17 +168,17 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests - name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests
merge-multiple: true merge-multiple: true
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub - name: Log in to docker hub
if: matrix.registry == 'dockerhub' if: matrix.registry == 'dockerhub'
@@ -219,19 +219,10 @@ jobs:
- init - init
- deploy-manifest - deploy-manifest
steps: steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: | script: |
let description = "ESPHome"; let description = "ESPHome";
if (context.eventName == "release") { if (context.eventName == "release") {
@@ -254,19 +245,10 @@ jobs:
needs: [init] needs: [init]
environment: ${{ needs.init.outputs.deploy_env }} environment: ${{ needs.init.outputs.deploy_env }}
steps: steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with: with:
github-token: ${{ steps.generate-token.outputs.token }} github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: | script: |
github.rest.actions.createWorkflowDispatch({ github.rest.actions.createWorkflowDispatch({
owner: "esphome", owner: "esphome",
@@ -277,34 +259,3 @@ jobs:
version: "${{ needs.init.outputs.tag }}", version: "${{ needs.init.outputs.tag }}",
} }
}) })
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Stale - name: Stale
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with: with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true remove-stale-when-updated: true

View File

@@ -14,7 +14,6 @@ jobs:
label: label:
- needs-docs - needs-docs
- merge-after-release - merge-after-release
- chained-pr
steps: steps:
- name: Check for ${{ matrix.label }} label - name: Check for ${{ matrix.label }} label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0

View File

@@ -13,16 +13,16 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
- name: Setup Python - name: Setup Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with: with:
python-version: 3.13 python-version: 3.13
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files python script/run-in-env.py pre-commit run --all-files
- name: Commit changes - name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0 uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with: with:
commit-message: "Synchronise Device Classes from Home Assistant" commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org> committer: esphomebot <esphome@openhomefoundation.org>

4
.gitignore vendored
View File

@@ -91,10 +91,6 @@ venv-*/
# mypy # mypy
.mypy_cache/ .mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs .pioenvs
.piolibdeps .piolibdeps
.pio .pio

View File

@@ -11,7 +11,7 @@ ci:
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.14.10 rev: v0.14.1
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -21,7 +21,6 @@ esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu esphome/components/ade7953_spi/* @angelnu
esphome/components/ads1118/* @solomondg1 esphome/components/ads1118/* @solomondg1
@@ -42,7 +41,6 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr esphome/components/as7341/* @mrgnr
@@ -74,7 +72,6 @@ esphome/components/bl0942/* @dbuezas @dwmw2
esphome/components/ble_client/* @buxtronix @clydebarrow esphome/components/ble_client/* @buxtronix @clydebarrow
esphome/components/ble_nus/* @tomaszduda23 esphome/components/ble_nus/* @tomaszduda23
esphome/components/bluetooth_proxy/* @bdraco @jesserockz esphome/components/bluetooth_proxy/* @bdraco @jesserockz
esphome/components/bm8563/* @abmantis
esphome/components/bme280_base/* @esphome/core esphome/components/bme280_base/* @esphome/core
esphome/components/bme280_spi/* @apbodrov esphome/components/bme280_spi/* @apbodrov
esphome/components/bme680_bsec/* @trvrnrth esphome/components/bme680_bsec/* @trvrnrth
@@ -98,7 +95,6 @@ esphome/components/camera_encoder/* @DT-art1
esphome/components/canbus/* @danielschramm @mvturnho esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97 esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @esphome/core esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret esphome/components/ch422g/* @clydebarrow @jesterret
@@ -159,14 +155,12 @@ esphome/components/esp32_ble_tracker/* @bdraco
esphome/components/esp32_camera_web_server/* @ayufan esphome/components/esp32_camera_web_server/* @ayufan
esphome/components/esp32_can/* @Sympatron esphome/components/esp32_can/* @Sympatron
esphome/components/esp32_hosted/* @swoboda1337 esphome/components/esp32_hosted/* @swoboda1337
esphome/components/esp32_hosted/update/* @swoboda1337
esphome/components/esp32_improv/* @jesserockz esphome/components/esp32_improv/* @jesserockz
esphome/components/esp32_rmt/* @jesserockz esphome/components/esp32_rmt/* @jesserockz
esphome/components/esp32_rmt_led_strip/* @jesserockz esphome/components/esp32_rmt_led_strip/* @jesserockz
esphome/components/esp8266/* @esphome/core esphome/components/esp8266/* @esphome/core
esphome/components/esp_ldo/* @clydebarrow esphome/components/esp_ldo/* @clydebarrow
esphome/components/espnow/* @jesserockz esphome/components/espnow/* @jesserockz
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
esphome/components/ethernet_info/* @gtjadsonsantos esphome/components/ethernet_info/* @gtjadsonsantos
esphome/components/event/* @nohat esphome/components/event/* @nohat
esphome/components/exposure_notifications/* @OttoWinter esphome/components/exposure_notifications/* @OttoWinter
@@ -185,14 +179,13 @@ esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval esphome/components/gl_r01_i2c/* @pkejval
esphome/components/globals/* @esphome/core esphome/components/globals/* @esphome/core
esphome/components/gp2y1010au0f/* @zry98 esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz @sebydocky esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle @ximex esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers esphome/components/gree/* @orestismers
esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246 esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte esphome/components/growatt_solar/* @leeuwte
@@ -207,16 +200,11 @@ esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann esphome/components/hbridge/light/* @DotNetDann
esphome/components/hbridge/switch/* @dwmw2 esphome/components/hbridge/switch/* @dwmw2
esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/he60r/* @clydebarrow esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2 esphome/components/hmac_md5/* @dwmw2
esphome/components/hmac_sha256/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/number/* @landonr esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004 esphome/components/homeassistant/switch/* @Links2004
@@ -230,7 +218,6 @@ esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering esphome/components/htu31d/* @betterengineering
esphome/components/hub75/* @stuartparmenter
esphome/components/hydreon_rgxx/* @functionpointer esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12 esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core esphome/components/i2c/* @esphome/core
@@ -300,7 +287,6 @@ esphome/components/mcp23x17_base/* @jesserockz
esphome/components/mcp23xxx_base/* @jesserockz esphome/components/mcp23xxx_base/* @jesserockz
esphome/components/mcp2515/* @danielschramm @mvturnho esphome/components/mcp2515/* @danielschramm @mvturnho
esphome/components/mcp3204/* @rsumner esphome/components/mcp3204/* @rsumner
esphome/components/mcp3221/* @philippderdiedas
esphome/components/mcp4461/* @p1ngb4ck esphome/components/mcp4461/* @p1ngb4ck
esphome/components/mcp4728/* @berfenger esphome/components/mcp4728/* @berfenger
esphome/components/mcp47a1/* @jesserockz esphome/components/mcp47a1/* @jesserockz
@@ -310,7 +296,7 @@ esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05 esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz @kahrendt esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
@@ -405,7 +391,6 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
esphome/components/rtl87xx/* @kuba2k2 esphome/components/rtl87xx/* @kuba2k2
esphome/components/rtttl/* @glmnet esphome/components/rtttl/* @glmnet
esphome/components/runtime_stats/* @bdraco esphome/components/runtime_stats/* @bdraco
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/scd4x/* @martgras @sjtrny esphome/components/scd4x/* @martgras @sjtrny
esphome/components/script/* @esphome/core esphome/components/script/* @esphome/core
@@ -469,7 +454,6 @@ esphome/components/st7735/* @SenexCrenshaw
esphome/components/st7789v/* @kbx81 esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155 esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004 esphome/components/statsd/* @Links2004
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter esphome/components/sun/* @OttoWinter
esphome/components/sun_gtil2/* @Mat931 esphome/components/sun_gtil2/* @Mat931
@@ -491,10 +475,8 @@ esphome/components/template/datetime/* @rfdarter
esphome/components/template/event/* @nohat esphome/components/template/event/* @nohat
esphome/components/template/fan/* @ssieb esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
esphome/components/thermopro_ble/* @sittner
esphome/components/thermostat/* @kbx81 esphome/components/thermostat/* @kbx81
esphome/components/time/* @esphome/core esphome/components/time/* @esphome/core
esphome/components/tinyusb/* @kbx81
esphome/components/tlc5947/* @rnauber esphome/components/tlc5947/* @rnauber
esphome/components/tlc5971/* @IJIJI esphome/components/tlc5971/* @IJIJI
esphome/components/tm1621/* @Philippe12 esphome/components/tm1621/* @Philippe12
@@ -519,7 +501,6 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ec/* @pvizeli
@@ -527,7 +508,6 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core esphome/components/valve/* @esphome/core
@@ -538,7 +518,6 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54 esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core esphome/components/web_server_base/* @esphome/core

View File

@@ -2,7 +2,7 @@
We welcome contributions to the ESPHome suite of code and documentation! We welcome contributions to the ESPHome suite of code and documentation!
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd). project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:** **See also:**

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2026.1.0-dev PROJECT_NUMBER = 2025.11.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description # 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 # for a project that appears at the top of each page and should give viewer a

View File

@@ -2,8 +2,8 @@
<a href="https://esphome.io/"> <a href="https://esphome.io/">
<picture> <picture>
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg"> <source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo"> <img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
</picture> </picture>
</a> </a>

View File

@@ -62,40 +62,6 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"external_components",
"<<",
}
)
def detect_external_components(config: ConfigType) -> set[str]:
"""Detect external/custom components in the configuration.
External components are those that appear in the config but are not
part of ESPHome's built-in components and are not special config keys.
Args:
config: The ESPHome configuration dictionary
Returns:
A set of external component names
"""
from esphome.analyze_memory.helpers import get_esphome_components
builtin_components = get_esphome_components()
return {
key
for key in config
if key not in builtin_components and key not in _NON_COMPONENT_KEYS
}
class ArgsProtocol(Protocol): class ArgsProtocol(Protocol):
device: list[str] | None device: list[str] | None
@@ -207,14 +173,14 @@ def choose_upload_log_host(
if has_mqtt_logging(): if has_mqtt_logging():
resolved.append("MQTT") resolved.append("MQTT")
if has_api() and has_non_ip_address() and has_resolvable_address(): if has_api() and has_non_ip_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING: elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup(): if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP") resolved.append("MQTTIP")
if has_ota() and has_non_ip_address() and has_resolvable_address(): if has_ota() and has_non_ip_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose)) resolved.extend(_resolve_with_cache(CORE.address, purpose))
else: else:
resolved.append(device) resolved.append(device)
@@ -318,17 +284,7 @@ def has_resolvable_address() -> bool:
"""Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address).""" """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
# Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
# The resolve_ip_address() function in helpers.py handles all types via AsyncResolver # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
if CORE.address is None: return CORE.address is not None
return False
if has_ip_address():
return True
if has_mdns():
return True
# .local mDNS hostnames are only resolvable if mDNS is enabled
return not CORE.address.endswith(".local")
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str): def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
@@ -518,49 +474,10 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose) rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0: if rc != 0:
return rc return rc
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config) idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1 return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool( def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int config: ConfigType, port: str, file: str, speed: int
) -> str | int: ) -> str | int:
@@ -780,13 +697,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None: def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config) exit_code = write_cpp(config)
if exit_code != 0: if exit_code != 0:
return exit_code return exit_code
@@ -982,72 +892,6 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
return 0 return 0
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
"""Analyze memory usage by component.
This command compiles the configuration and performs memory analysis.
Compilation is fast if sources haven't changed (just relinking).
"""
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)
if exit_code != 0:
return exit_code
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
_LOGGER.info("Successfully compiled program.")
# Get idedata for analysis
idedata = platformio_api.get_idedata(config)
if idedata is None:
_LOGGER.error("Failed to get IDE data for memory analysis")
return 1
firmware_elf = Path(idedata.firmware_elf_path)
# Extract external components from config
external_components = detect_external_components(config)
_LOGGER.debug("Detected external components: %s", external_components)
# Perform component memory analysis
_LOGGER.info("Analyzing memory usage...")
analyzer = MemoryAnalyzerCLI(
str(firmware_elf),
idedata.objdump_path,
idedata.readelf_path,
external_components,
)
analyzer.analyze()
# 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
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None: def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
new_name = args.name new_name = args.name
for c in new_name: for c in new_name:
@@ -1163,7 +1007,6 @@ POST_CONFIG_ACTIONS = {
"idedata": command_idedata, "idedata": command_idedata,
"rename": command_rename, "rename": command_rename,
"discover": command_discover, "discover": command_discover,
"analyze-memory": command_analyze_memory,
} }
SIMPLE_CONFIG_ACTIONS = [ SIMPLE_CONFIG_ACTIONS = [
@@ -1266,17 +1109,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.", help="Only generate source code, do not compile.",
action="store_true", action="store_true",
) )
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser( parser_upload = subparsers.add_parser(
"upload", "upload",
@@ -1394,7 +1226,7 @@ def parse_args(argv):
"clean-all", help="Clean all build and platform files." "clean-all", help="Clean all build and platform files."
) )
parser_clean_all.add_argument( parser_clean_all.add_argument(
"configuration", help="Your YAML file or configuration directory.", nargs="*" "configuration", help="Your YAML configuration directory.", nargs="*"
) )
parser_dashboard = subparsers.add_parser( parser_dashboard = subparsers.add_parser(
@@ -1460,14 +1292,6 @@ def parse_args(argv):
) )
parser_rename.add_argument("name", help="The new name for the device.", type=str) parser_rename.add_argument("name", help="The new name for the device.", type=str)
parser_analyze_memory = subparsers.add_parser(
"analyze-memory",
help="Analyze memory usage by component.",
)
parser_analyze_memory.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
# Keep backward compatibility with the old command line format of # Keep backward compatibility with the old command line format of
# esphome <config> <command>. # esphome <config> <command>.
# #

View File

@@ -15,7 +15,6 @@ from .const import (
SECTION_TO_ATTR, SECTION_TO_ATTR,
SYMBOL_PATTERNS, SYMBOL_PATTERNS,
) )
from .demangle import batch_demangle
from .helpers import ( from .helpers import (
get_component_class_patterns, get_component_class_patterns,
get_esphome_components, get_esphome_components,
@@ -28,6 +27,15 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__) _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 # C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"]) _CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
@@ -304,9 +312,168 @@ class MemoryAnalyzer:
if not symbols: if not symbols:
return return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols)) _LOGGER.info("Demangling %d symbols", len(symbols))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path) _LOGGER.debug("objdump_path = %s", self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
# 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
def _demangle_symbol(self, symbol: str) -> str: def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache.""" """Get demangled C++ symbol name from cache."""

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation.""" """CLI interface for memory analysis with report generation."""
from collections import defaultdict from collections import defaultdict
import json
import sys import sys
from . import ( from . import (
@@ -16,11 +15,6 @@ from . import (
class MemoryAnalyzerCLI(MemoryAnalyzer): class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation.""" """Memory analyzer with CLI-specific report generation."""
# Symbol size threshold for detailed analysis
SYMBOL_SIZE_THRESHOLD: int = (
100 # Show symbols larger than this in detailed analysis
)
# Column width constants # Column width constants
COL_COMPONENT: int = 29 COL_COMPONENT: int = 29
COL_FLASH_TEXT: int = 14 COL_FLASH_TEXT: int = 14
@@ -197,21 +191,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%" f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
) )
# All core symbols above threshold # Top 15 largest core symbols
lines.append("") lines.append("")
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
sorted_core_symbols = sorted( sorted_core_symbols = sorted(
self._esphome_core_symbols, key=lambda x: x[2], reverse=True self._esphome_core_symbols, key=lambda x: x[2], reverse=True
) )
large_core_symbols = [
(symbol, demangled, size)
for symbol, demangled, size in sorted_core_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
]
lines.append( for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH) lines.append("=" * self.TABLE_WIDTH)
@@ -244,22 +231,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
api_component = (name, mem) api_component = (name, mem)
break break
# Also include wifi_stack and other important system components if they exist # Combine all components to analyze: top ESPHome + all external + API if not already included
system_components_to_include = [ components_to_analyze = list(top_esphome_components) + list(
# Empty list - we've finished debugging symbol categorization top_external_components
# Add component names here if you need to debug their symbols
]
system_components = [
(name, mem)
for name, mem in components
if name in system_components_to_include
]
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
components_to_analyze = (
list(top_esphome_components)
+ list(top_external_components)
+ system_components
) )
if api_component and api_component not in components_to_analyze: if api_component and api_component not in components_to_analyze:
components_to_analyze.append(api_component) components_to_analyze.append(api_component)
@@ -281,15 +255,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f"Total size: {comp_mem.flash_total:,} B") lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("") lines.append("")
# Show all symbols above threshold for better visibility # Show all symbols > 100 bytes for better visibility
large_symbols = [ large_symbols = [
(sym, dem, size) (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
for sym, dem, size in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
] ]
lines.append( lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):" f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
) )
for i, (symbol, demangled, size) in enumerate(large_symbols): for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)") lines.append(f"{i + 1}. {demangled} ({size:,} B)")
@@ -298,28 +270,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines) return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None: def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis.""" """Dump uncategorized symbols for analysis."""
# Sort by size descending # Sort by size descending

View File

@@ -127,39 +127,40 @@ SYMBOL_PATTERNS = {
"tryget_socket_unconn", "tryget_socket_unconn",
"cs_create_ctrl_sock", "cs_create_ctrl_sock",
"netbuf_alloc", "netbuf_alloc",
"tcp_", # TCP protocol functions
"udp_", # UDP protocol functions
"lwip_", # LwIP stack functions
"eagle_lwip", # ESP-specific LwIP functions
"new_linkoutput", # Link output function
"acd_", # Address Conflict Detection (ACD)
"eth_", # Ethernet functions
"mac_enable_bb", # MAC baseband enable
"reassemble_and_dispatch", # Packet reassembly
], ],
# dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
"dhcp": ["dhcp", "handle_dhcp"],
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"], "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones. "wifi_stack": [
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern "ieee80211",
"mdns_lib": ["mdns"], "hostap",
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols "sta_",
"memory_mgmt": [ "ap_",
"mem_", "scan_",
"memory_", "wifi_",
"tlsf_", "wpa_",
"memp_", "wps_",
"pbuf_", "esp_wifi",
"pbuf_alloc", "cnx_",
"pbuf_copy_partial_pbuf", "wpa3_",
"esp_mmu_map", "sae_",
"mmu_hal_", "wDev_",
"s_do_mapping", # Memory mapping function, not WiFi "ic_",
"hash_map_", # Hash map data structure "mac_",
"umm_assimilate", # UMM malloc assimilation "esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
"s_do_mapping",
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
], ],
# Bluetooth categories must come BEFORE wifi_stack to avoid misclassification "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
# Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns "wifi_bt_coex": ["coex"],
"bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"], "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
"bluedroid_bt": [ "bluedroid_bt": [
"bluedroid", "bluedroid",
@@ -206,61 +207,6 @@ SYMBOL_PATTERNS = {
"copy_extra_byte_in_db", "copy_extra_byte_in_db",
"parse_read_local_supported_commands_response", "parse_read_local_supported_commands_response",
], ],
"bluetooth": [
"bt_",
"_ble_", # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
"l2c_",
"l2ble_", # L2CAP for BLE
"gatt_",
"gap_",
"hci_",
"btsnd_hcic_", # Bluetooth HCI command send functions
"BT_init",
"BT_tx_", # Bluetooth transmit functions
"esp_ble_", # Catch esp_ble_* functions
],
"bluetooth_ll": [
"llm_", # Link layer manager
"llc_", # Link layer control
"lld_", # Link layer driver
"ld_acl_", # Link layer ACL (Asynchronous Connection-Oriented)
"llcp_", # Link layer control protocol
"lmp_", # Link manager protocol
],
"wifi_bt_coex": ["coex"],
"wifi_stack": [
"ieee80211",
"hostap",
"sta_",
"wifi_ap_", # More specific than "ap_" to avoid matching "cap_", "map_"
"wifi_scan_", # More specific than "scan_" to avoid matching "_scan_" in other contexts
"wifi_",
"wpa_",
"wps_",
"esp_wifi",
"cnx_",
"wpa3_",
"sae_",
"wDev_",
"ic_mac_", # More specific than "mac_" to avoid matching emac_
"esf_buf",
"gWpaSm",
"sm_WPA",
"eapol_",
"owe_",
"wifiLowLevelInit",
# Removed "s_do_mapping" - this is memory management, not WiFi
"gScanStruct",
"ppSearchTxframe",
"ppMapWaitTxq",
"ppFillAMPDUBar",
"ppCheckTxConnTrafficIdle",
"ppCalTkipMic",
"phy_force_wifi",
"phy_unforce_wifi",
"write_wifi_chan",
"wifi_track_pll",
],
"crypto_math": [ "crypto_math": [
"ecp_", "ecp_",
"bignum_", "bignum_",
@@ -285,36 +231,13 @@ SYMBOL_PATTERNS = {
"p_256_init_curve", "p_256_init_curve",
"shift_sub_rows", "shift_sub_rows",
"rshift", "rshift",
"rijndaelEncrypt", # AES Rijndael encryption
],
# System and Arduino core functions must come before libc
"esp_system": [
"system_", # ESP system functions
"postmortem_", # Postmortem reporting
],
"arduino_core": [
"pinMode",
"resetPins",
"millis",
"micros",
"delay(", # More specific - Arduino delay function with parenthesis
"delayMicroseconds",
"digitalWrite",
"digitalRead",
],
"sntp": ["sntp_", "sntp_recv"],
"scheduler": [
"run_scheduled_",
"compute_scheduled_",
"event_TaskQueue",
], ],
"hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"], "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
"libc": [ "libc": [
"printf", "printf",
"scanf", "scanf",
"malloc", "malloc",
"_free", # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring "free",
"umm_free", # UMM malloc free function
"memcpy", "memcpy",
"memset", "memset",
"strcpy", "strcpy",
@@ -336,7 +259,7 @@ SYMBOL_PATTERNS = {
"_setenv_r", "_setenv_r",
"_tzset_unlocked_r", "_tzset_unlocked_r",
"__tzcalc_limits", "__tzcalc_limits",
"_select", # More specific than "select" to avoid matching "dhcp_select", etc. "select",
"scalbnf", "scalbnf",
"strtof", "strtof",
"strtof_l", "strtof_l",
@@ -393,24 +316,8 @@ SYMBOL_PATTERNS = {
"CSWTCH$", "CSWTCH$",
"dst$", "dst$",
"sulp", "sulp",
"_strtol_l", # String to long with locale
"__cvt", # Convert
"__utoa", # Unsigned to ASCII
"__global_locale", # Global locale
"_ctype_", # Character type
"impure_data", # Impure data
],
"string_ops": [
"strcmp",
"strncmp",
"strchr",
"strstr",
"strtok",
"strdup",
"strncasecmp_P", # String compare (case insensitive, from program memory)
"strnlen_P", # String length (from program memory)
"strncat_P", # String concatenate (from program memory)
], ],
"string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
"memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"], "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
"file_io": [ "file_io": [
"fread", "fread",
@@ -431,26 +338,10 @@ SYMBOL_PATTERNS = {
"vsscanf", "vsscanf",
], ],
"cpp_anonymous": ["_GLOBAL__N_", "n$"], "cpp_anonymous": ["_GLOBAL__N_", "n$"],
# Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
"nvs": ["nvs_"], # Plain C NVS functions "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
"ota": ["ota_", "OTA", "esp_ota", "app_desc"],
# cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
# Only keep patterns that are truly runtime-specific and not categorizable by namespace
"cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
"exception_handling": [
"__cxa_",
"_Unwind_",
"__gcc_personality",
"uw_frame_state",
"search_object", # Search for exception handling object
"get_cie_encoding", # Get CIE encoding
"add_fdes", # Add frame description entries
"fde_unencoded_compare", # Compare FDEs
"fde_mixed_encoding_compare", # Compare mixed encoding FDEs
"frame_downheap", # Frame heap operations
"frame_heapsort", # Frame heap sorting
],
"static_init": ["_GLOBAL__sub_I_"], "static_init": ["_GLOBAL__sub_I_"],
"mdns_lib": ["mdns"],
"phy_radio": [ "phy_radio": [
"phy_", "phy_",
"rf_", "rf_",
@@ -503,47 +394,10 @@ SYMBOL_PATTERNS = {
"txcal_debuge_mode", "txcal_debuge_mode",
"ant_wifitx_cfg", "ant_wifitx_cfg",
"reg_init_begin", "reg_init_begin",
"tx_cap_init", # TX capacitance init
"ram_set_txcap", # RAM TX capacitance setting
"tx_atten_", # TX attenuation
"txiq_", # TX I/Q calibration
"ram_cal_", # RAM calibration
"ram_rxiq_", # RAM RX I/Q
"readvdd33", # Read VDD33
"test_tout", # Test timeout
"tsen_meas", # Temperature sensor measurement
"bbpll_cal", # Baseband PLL calibration
"set_cal_", # Set calibration
"set_rfanagain_", # Set RF analog gain
"set_txdc_", # Set TX DC
"get_vdd33_", # Get VDD33
"gen_rx_gain_table", # Generate RX gain table
"ram_ana_inf_gating_en", # RAM analog interface gating enable
"tx_cont_en", # TX continuous enable
"tx_delay_cfg", # TX delay configuration
"tx_gain_table_set", # TX gain table set
"check_and_reset_hw_deadlock", # Hardware deadlock check
"s_config", # System/hardware config
"chan14_mic_cfg", # Channel 14 MIC config
],
"wifi_phy_pp": [
"pp_",
"ppT",
"ppR",
"ppP",
"ppInstall",
"ppCalTxAMPDULength",
"ppCheckTx", # Packet processor TX check
"ppCal", # Packet processor calibration
"HdlAllBuffedEb", # Handle buffered EB
], ],
"wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
"wifi_lmac": ["lmac"], "wifi_lmac": ["lmac"],
"wifi_device": [ "wifi_device": ["wdev", "wDev_"],
"wdev",
"wDev_",
"ic_set_sta", # Set station mode
"ic_set_vif", # Set virtual interface
],
"power_mgmt": [ "power_mgmt": [
"pm_", "pm_",
"sleep", "sleep",
@@ -552,7 +406,15 @@ SYMBOL_PATTERNS = {
"deep_sleep", "deep_sleep",
"power_down", "power_down",
"g_pm", "g_pm",
"pmc", # Power Management Controller ],
"memory_mgmt": [
"mem_",
"memory_",
"tlsf_",
"memp_",
"pbuf_",
"pbuf_alloc",
"pbuf_copy_partial_pbuf",
], ],
"hal_layer": ["hal_"], "hal_layer": ["hal_"],
"clock_mgmt": [ "clock_mgmt": [
@@ -577,6 +439,7 @@ SYMBOL_PATTERNS = {
"error_handling": ["panic", "abort", "assert", "error_", "fault"], "error_handling": ["panic", "abort", "assert", "error_", "fault"],
"authentication": ["auth"], "authentication": ["auth"],
"ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"], "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
"dhcp": ["dhcp", "handle_dhcp"],
"ethernet_phy": [ "ethernet_phy": [
"emac_", "emac_",
"eth_phy_", "eth_phy_",
@@ -755,15 +618,7 @@ SYMBOL_PATTERNS = {
"ampdu_dispatch_upto", "ampdu_dispatch_upto",
], ],
"ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"], "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
"rate_control": [ "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
"rssi_margin",
"rcGetSched",
"get_rate_fcc_index",
"rcGetRate", # Get rate
"rc_get_", # Rate control getters
"rc_set_", # Rate control setters
"rc_enable_", # Rate control enable functions
],
"nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"], "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
"channel_mgmt": ["chm_init", "chm_set_current_channel"], "channel_mgmt": ["chm_init", "chm_set_current_channel"],
"trace": ["trc_init", "trc_onAmpduOp"], "trace": ["trc_init", "trc_onAmpduOp"],
@@ -944,18 +799,31 @@ SYMBOL_PATTERNS = {
"supports_interlaced_inquiry_scan", "supports_interlaced_inquiry_scan",
"supports_reading_remote_extended_features", "supports_reading_remote_extended_features",
], ],
"bluetooth_ll": [
"lld_pdu_",
"ld_acl_",
"lld_stop_ind_handler",
"lld_evt_winsize_change",
"config_lld_evt_funcs_reset",
"config_lld_funcs_reset",
"config_llm_funcs_reset",
"llm_set_long_adv_data",
"lld_retry_tx_prog",
"llc_link_sup_to_ind_handler",
"config_llc_funcs_reset",
"lld_evt_rxwin_compute",
"config_btdm_funcs_reset",
"config_ea_funcs_reset",
"llc_defalut_state_tab_reset",
"config_rwip_funcs_reset",
"ke_lmp_rx_flooding_detect",
],
} }
# Demangled patterns: patterns found in demangled C++ names # Demangled patterns: patterns found in demangled C++ names
DEMANGLED_PATTERNS = { DEMANGLED_PATTERNS = {
"gpio_driver": ["GPIO"], "gpio_driver": ["GPIO"],
"uart_driver": ["UART"], "uart_driver": ["UART"],
# mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
"mdns_lib": [
"MDNSResponder",
"MDNSImplementation",
"MDNS",
],
"network_stack": [ "network_stack": [
"lwip", "lwip",
"tcp", "tcp",
@@ -968,24 +836,6 @@ DEMANGLED_PATTERNS = {
"ethernet", "ethernet",
"ppp", "ppp",
"slip", "slip",
"UdpContext", # UDP context class
"DhcpServer", # DHCP server class
],
"arduino_core": [
"String::", # Arduino String class
"Print::", # Arduino Print class
"HardwareSerial::", # Serial class
"IPAddress::", # IP address class
"EspClass::", # ESP class
"experimental::_SPI", # Experimental SPI
],
"ota": [
"UpdaterClass",
"Updater::",
],
"wifi": [
"ESP8266WiFi",
"WiFi::",
], ],
"wifi_stack": ["NetworkInterface"], "wifi_stack": ["NetworkInterface"],
"nimble_bt": [ "nimble_bt": [
@@ -1004,6 +854,7 @@ DEMANGLED_PATTERNS = {
"rtti": ["__type_info", "__class_type_info"], "rtti": ["__type_info", "__class_type_info"],
"web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"], "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
"async_tcp": ["AsyncClient", "AsyncServer"], "async_tcp": ["AsyncClient", "AsyncServer"],
"mdns_lib": ["mdns"],
"json_lib": [ "json_lib": [
"ArduinoJson", "ArduinoJson",
"JsonDocument", "JsonDocument",

View File

@@ -1,182 +0,0 @@
"""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

View File

@@ -1,493 +0,0 @@
"""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)

View File

@@ -1,57 +0,0 @@
"""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

View File

@@ -15,13 +15,8 @@ from esphome.const import (
CONF_TYPE_ID, CONF_TYPE_ID,
CONF_UPDATE_INTERVAL, CONF_UPDATE_INTERVAL,
) )
from esphome.core import ID, Lambda from esphome.core import ID
from esphome.cpp_generator import ( from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
LambdaExpression,
MockObj,
MockObjClass,
TemplateArgsType,
)
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType from esphome.types import ConfigType
from esphome.util import Registry from esphome.util import Registry
@@ -92,7 +87,6 @@ def validate_potentially_or_condition(value):
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component) DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action) LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action) IfAction = cg.esphome_ns.class_("IfAction", Action)
WhileAction = cg.esphome_ns.class_("WhileAction", Action) WhileAction = cg.esphome_ns.class_("WhileAction", Action)
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action) RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
@@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
Automation = cg.esphome_ns.class_("Automation") Automation = cg.esphome_ns.class_("Automation")
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
def new_lambda_pvariable(
id_obj: ID,
lambda_expr: LambdaExpression,
stateless_class: MockObjClass,
template_arg: cg.TemplateArguments | None = None,
) -> MockObj:
"""Create Pvariable for lambda, using stateless class if applicable.
Combines ID selection and Pvariable creation in one call. For stateless
lambdas (empty capture), uses function pointer instead of std::function.
Args:
id_obj: The ID object (action_id, condition_id, or filter_id)
lambda_expr: The lambda expression object
stateless_class: The stateless class to use for stateless lambdas
template_arg: Optional template arguments (for actions/conditions)
Returns:
The created Pvariable
"""
# For stateless lambdas, use function pointer instead of std::function
if lambda_expr.capture == "":
id_obj = id_obj.copy()
id_obj.type = stateless_class
if template_arg is not None:
return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
return cg.new_Pvariable(id_obj, lambda_expr)
def validate_automation(extra_schema=None, extra_validators=None, single=False): def validate_automation(extra_schema=None, extra_validators=None, single=False):
if extra_schema is None: if extra_schema is None:
extra_schema = {} extra_schema = {}
@@ -182,7 +145,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: if len(value) != 1:
raise cv.Invalid("This trigger allows only a single automation") raise cv.Invalid("Cannot have more than 1 automation for templates")
return value[0] return value[0]
return value return value
@@ -277,9 +240,7 @@ async def lambda_condition_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=bool) lambda_ = await cg.process_lambda(config, args, return_type=bool)
return new_lambda_pvariable( return cg.new_Pvariable(condition_id, template_arg, lambda_)
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_condition( @register_condition(
@@ -310,30 +271,6 @@ async def for_condition_to_code(
return var return var
@register_condition(
"component.is_idle",
LambdaCondition,
maybe_simple_id(
{
cv.Required(CONF_ID): cv.use_id(cg.Component),
}
),
)
async def component_is_idle_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
comp = await cg.get_variable(config[CONF_ID])
lambda_ = await cg.process_lambda(
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
)
return new_lambda_pvariable(
condition_id, lambda_, StatelessLambdaCondition, template_arg
)
@register_action( @register_action(
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds) "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
) )
@@ -469,7 +406,7 @@ async def lambda_action_to_code(
args: TemplateArgsType, args: TemplateArgsType,
) -> MockObj: ) -> MockObj:
lambda_ = await cg.process_lambda(config, args, return_type=cg.void) lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg) return cg.new_Pvariable(action_id, template_arg, lambda_)
@register_action( @register_action(

View File

@@ -62,7 +62,6 @@ from esphome.cpp_types import ( # noqa: F401
EntityBase, EntityBase,
EntityCategory, EntityCategory,
ESPTime, ESPTime,
FixedVector,
GPIOPin, GPIOPin,
InternalGPIOPin, InternalGPIOPin,
JsonObject, JsonObject,

View File

@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
break; break;
default: default:
this->publish_state(NAN); this->publish_state(NAN);
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!")); this->status_set_error("Invalid saturation vapor pressure equation selection!");
return; return;
} }
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); 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/ // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
// H/T to https://esphome.io/cookbook/bme280_environment/ // H/T to https://esphome.io/cookbook/bme280_environment.html
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ // 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) { float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// es = saturated vapor pressure (kPa) // es = saturated vapor pressure (kPa)

View File

@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
void AdalightLightEffect::start() { void AdalightLightEffect::start() {
AddressableLightEffect::start(); AddressableLightEffect::start();

View File

@@ -11,7 +11,7 @@ namespace adalight {
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
public: public:
AdalightLightEffect(const char *name); AdalightLightEffect(const std::string &name);
void start() override; void start() override;
void stop() override; void stop() override;

View File

@@ -1,17 +1,15 @@
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components.esp32 import ( from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5, VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
get_esp32_variant,
) )
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266 from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
@@ -101,13 +99,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
5: adc_channel_t.ADC_CHANNEL_5, 5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6, 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: { VARIANT_ESP32H2: {
1: adc_channel_t.ADC_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
@@ -116,17 +107,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
4: adc_channel_t.ADC_CHANNEL_3, 4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4, 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
1: adc_channel_t.ADC_CHANNEL_0, 1: adc_channel_t.ADC_CHANNEL_0,
@@ -153,6 +133,16 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8, 9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9, 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 # pin to adc2 channel mapping
@@ -183,19 +173,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C5: {}, # no ADC2 VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2 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 # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: { VARIANT_ESP32S2: {
11: adc_channel_t.ADC_CHANNEL_0, 11: adc_channel_t.ADC_CHANNEL_0,
@@ -222,6 +201,14 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8, 19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9, 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,
},
} }

View File

@@ -42,11 +42,10 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_; init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE; init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || #endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 // USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -75,7 +74,7 @@ void ADCSensor::setup() {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
// RISC-V variants and S3 use curve fitting calibration // RISC-V variants and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
@@ -112,7 +111,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false; this->setup_flags_.calibration_complete = false;
} }
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
} }
this->setup_flags_.init_complete = true; this->setup_flags_.init_complete = true;
@@ -187,11 +186,11 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err); ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else // Other ESP32 variants use line fitting calibration #else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 #endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr; this->calibration_handle_ = nullptr;
} }
} }
@@ -220,7 +219,7 @@ float ADCSensor::sample_autorange_() {
if (this->calibration_handle_ != nullptr) { if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle // Delete old calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#else #else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_); adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
@@ -232,7 +231,7 @@ float ADCSensor::sample_autorange_() {
adc_cali_handle_t handle = nullptr; adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_curve_fitting_config_t cali_config = {}; adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_; cali_config.chan = this->channel_;
@@ -267,7 +266,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err); ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) { if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);
@@ -289,7 +288,7 @@ float ADCSensor::sample_autorange_() {
} }
// Clean up calibration handle // Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
adc_cali_delete_scheme_curve_fitting(handle); adc_cali_delete_scheme_curve_fitting(handle);
#else #else
adc_cali_delete_scheme_line_fitting(handle); adc_cali_delete_scheme_line_fitting(handle);

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{ {
cv.GenerateID(): cv.declare_id(ADE7880), cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All( cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.float_range(min=45.0, max=66.0) cv.frequency, cv.Range(min=45.0, max=66.0)
), ),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema, cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema, cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -24,8 +24,6 @@ from esphome.const import (
UNIT_WATT, UNIT_WATT,
) )
CODEOWNERS = ["@angelnu"]
CONF_CURRENT_A = "current_a" CONF_CURRENT_A = "current_a"
CONF_CURRENT_B = "current_b" CONF_CURRENT_B = "current_b"
CONF_ACTIVE_POWER_A = "active_power_a" CONF_ACTIVE_POWER_A = "active_power_a"

View File

@@ -25,8 +25,7 @@ void ADE7953::setup() {
this->ade_write_8(PGA_V_8, pga_v_); this->ade_write_8(PGA_V_8, pga_v_);
this->ade_write_8(PGA_IA_8, pga_ia_); this->ade_write_8(PGA_IA_8, pga_ia_);
this->ade_write_8(PGA_IB_8, pga_ib_); this->ade_write_8(PGA_IB_8, pga_ib_);
this->ade_write_32(AVGAIN_32, avgain_); this->ade_write_32(AVGAIN_32, vgain_);
this->ade_write_32(BVGAIN_32, bvgain_);
this->ade_write_32(AIGAIN_32, aigain_); this->ade_write_32(AIGAIN_32, aigain_);
this->ade_write_32(BIGAIN_32, bigain_); this->ade_write_32(BIGAIN_32, bigain_);
this->ade_write_32(AWGAIN_32, awgain_); this->ade_write_32(AWGAIN_32, awgain_);
@@ -35,8 +34,7 @@ void ADE7953::setup() {
this->ade_read_8(PGA_V_8, &pga_v_); this->ade_read_8(PGA_V_8, &pga_v_);
this->ade_read_8(PGA_IA_8, &pga_ia_); this->ade_read_8(PGA_IA_8, &pga_ia_);
this->ade_read_8(PGA_IB_8, &pga_ib_); this->ade_read_8(PGA_IB_8, &pga_ib_);
this->ade_read_32(AVGAIN_32, &avgain_); this->ade_read_32(AVGAIN_32, &vgain_);
this->ade_read_32(BVGAIN_32, &bvgain_);
this->ade_read_32(AIGAIN_32, &aigain_); this->ade_read_32(AIGAIN_32, &aigain_);
this->ade_read_32(BIGAIN_32, &bigain_); this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_); this->ade_read_32(AWGAIN_32, &awgain_);
@@ -65,14 +63,13 @@ void ADE7953::dump_config() {
" PGA_V_8: 0x%X\n" " PGA_V_8: 0x%X\n"
" PGA_IA_8: 0x%X\n" " PGA_IA_8: 0x%X\n"
" PGA_IB_8: 0x%X\n" " PGA_IB_8: 0x%X\n"
" AVGAIN_32: 0x%08jX\n" " VGAIN_32: 0x%08jX\n"
" BVGAIN_32: 0x%08jX\n"
" AIGAIN_32: 0x%08jX\n" " AIGAIN_32: 0x%08jX\n"
" BIGAIN_32: 0x%08jX\n" " BIGAIN_32: 0x%08jX\n"
" AWGAIN_32: 0x%08jX\n" " AWGAIN_32: 0x%08jX\n"
" BWGAIN_32: 0x%08jX", " BWGAIN_32: 0x%08jX",
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_, this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_); (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
} }
#define ADE_PUBLISH_(name, val, factor) \ #define ADE_PUBLISH_(name, val, factor) \

View File

@@ -46,12 +46,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; } void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
// Set input gains // Set input gains
void set_vgain(uint32_t vgain) { void set_vgain(uint32_t vgain) { vgain_ = vgain; }
// Datasheet says: "to avoid discrepancies in other registers,
// if AVGAIN is set then BVGAIN should be set to the same value."
avgain_ = vgain;
bvgain_ = vgain;
}
void set_aigain(uint32_t aigain) { aigain_ = aigain; } void set_aigain(uint32_t aigain) { aigain_ = aigain; }
void set_bigain(uint32_t bigain) { bigain_ = bigain; } void set_bigain(uint32_t bigain) { bigain_ = bigain; }
void set_awgain(uint32_t awgain) { awgain_ = awgain; } void set_awgain(uint32_t awgain) { awgain_ = awgain; }
@@ -105,8 +100,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint8_t pga_v_; uint8_t pga_v_;
uint8_t pga_ia_; uint8_t pga_ia_;
uint8_t pga_ib_; uint8_t pga_ib_;
uint32_t avgain_; uint32_t vgain_;
uint32_t bvgain_;
uint32_t aigain_; uint32_t aigain_;
uint32_t bigain_; uint32_t bigain_;
uint32_t awgain_; uint32_t awgain_;

View File

@@ -105,7 +105,7 @@ template<typename... Ts> class AGS10NewI2cAddressAction : public Action<Ts...>,
public: public:
TEMPLATABLE_VALUE(uint8_t, new_address) TEMPLATABLE_VALUE(uint8_t, new_address)
void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); } void play(Ts... x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
}; };
enum AGS10SetZeroPointActionMode { enum AGS10SetZeroPointActionMode {
@@ -122,7 +122,7 @@ template<typename... Ts> class AGS10SetZeroPointAction : public Action<Ts...>, p
TEMPLATABLE_VALUE(uint16_t, value) TEMPLATABLE_VALUE(uint16_t, value)
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode) TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
void play(const Ts &...x) override { void play(Ts... x) override {
switch (this->mode_.value(x...)) { switch (this->mode_.value(x...)) {
case FACTORY_DEFAULT: case FACTORY_DEFAULT:
this->parent_->set_zero_point_with_factory_defaults(); this->parent_->set_zero_point_with_factory_defaults();

View File

@@ -83,7 +83,7 @@ void AHT10Component::setup() {
void AHT10Component::restart_read_() { void AHT10Component::restart_read_() {
if (this->read_count_ == AHT10_ATTEMPTS) { if (this->read_count_ == AHT10_ATTEMPTS) {
this->read_count_ = 0; this->read_count_ = 0;
this->status_set_error(LOG_STR("Reading timed out")); this->status_set_error("Reading timed out");
return; return;
} }
this->read_count_++; this->read_count_++;

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetAutoMuteAction : public Action<Ts...> {
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode) TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); } void play(Ts... x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
protected: protected:
AIC3204 *aic3204_; AIC3204 *aic3204_;

View File

@@ -172,6 +172,12 @@ def alarm_control_panel_schema(
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema) return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.GenerateID(): cv.use_id(AlarmControlPanel), cv.GenerateID(): cv.use_id(AlarmControlPanel),

View File

@@ -1,9 +1,7 @@
#include "alarm_control_panel.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <utility> #include <utility>
#include "alarm_control_panel.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -35,12 +33,23 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state; this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call(); this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY) if (state == ACP_STATE_TRIGGERED) {
ControllerRegistry::notify_alarm_control_panel_update(this); this->triggered_callback_.call();
#endif } else if (state == ACP_STATE_ARMING) {
// Cleared fires when leaving TRIGGERED state this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
if (prev_state == ACP_STATE_TRIGGERED) { if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call(); this->cleared_callback_.call();
} }
@@ -55,6 +64,34 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback)); this->state_callback_.add(std::move(callback));
} }
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) { void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback)); this->cleared_callback_.add(std::move(callback));
} }

View File

@@ -35,13 +35,54 @@ class AlarmControlPanel : public EntityBase {
*/ */
void publish_state(AlarmControlPanelState state); void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes. /** Add a callback for when the state of the alarm_control_panel changes
* Triggers can check get_state() to determine the new state.
* *
* @param callback The callback function * @param callback The callback function
*/ */
void add_on_state_callback(std::function<void()> &&callback); void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered /** Add a callback for when the state of the alarm_control_panel clears from triggered
* *
* @param callback The callback function * @param callback The callback function
@@ -131,14 +172,28 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_; uint32_t last_update_;
// the call control function // the call control function
virtual void control(const AlarmControlPanelCall &call) = 0; virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state // state callback
LazyCallbackManager<void()> state_callback_{}; CallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state // trigger callback
LazyCallbackManager<void()> cleared_callback_{}; CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// chime callback // chime callback
LazyCallbackManager<void()> chime_callback_{}; CallbackManager<void()> chime_callback_{};
// ready callback // ready callback
LazyCallbackManager<void()> ready_callback_{}; CallbackManager<void()> ready_callback_{};
}; };
} // namespace alarm_control_panel } // namespace alarm_control_panel

View File

@@ -6,7 +6,6 @@
namespace esphome { namespace esphome {
namespace alarm_control_panel { namespace alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> { class StateTrigger : public Trigger<> {
public: public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -14,30 +13,55 @@ class StateTrigger : public Trigger<> {
} }
}; };
/// Template trigger that fires when entering a specific state class TriggeredTrigger : public Trigger<> {
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
public: public:
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) { explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() { alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
} }
protected:
AlarmControlPanel *alarm_control_panel_;
}; };
// Type aliases for state-specific triggers class ArmingTrigger : public Trigger<> {
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>; public:
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>; explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>; alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>; }
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>; };
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>; class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> { class ClearedTrigger : public Trigger<> {
public: public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -45,7 +69,6 @@ class ClearedTrigger : public Trigger<> {
} }
}; };
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> { class ChimeTrigger : public Trigger<> {
public: public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) { explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -53,7 +76,6 @@ class ChimeTrigger : public Trigger<> {
} }
}; };
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> { class ReadyTrigger : public Trigger<> {
public: public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) { explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -67,7 +89,7 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -87,7 +109,7 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -107,7 +129,7 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->alarm_control_panel_->make_call(); auto call = this->alarm_control_panel_->make_call();
auto code = this->code_.optional_value(x...); auto code = this->code_.optional_value(x...);
if (code.has_value()) { if (code.has_value()) {
@@ -127,7 +149,7 @@ template<typename... Ts> class DisarmAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, code) TEMPLATABLE_VALUE(std::string, code)
void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -137,7 +159,7 @@ template<typename... Ts> class PendingAction : public Action<Ts...> {
public: public:
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -147,7 +169,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
public: public:
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); } void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
protected: protected:
AlarmControlPanel *alarm_control_panel_; AlarmControlPanel *alarm_control_panel_;
@@ -156,7 +178,7 @@ template<typename... Ts> class TriggeredAction : public Action<Ts...> {
template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> { template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> {
public: public:
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
bool check(const Ts &...x) override { bool check(Ts... x) override {
return this->parent_->is_state_armed(this->parent_->get_state()) || return this->parent_->is_state_armed(this->parent_->get_state()) ||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
} }

View File

@@ -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) { void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
if (this->response_offset_ >= this->response_length_) { if (this->response_offset_ >= this->response_length_) {
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
if (length < GENI_RESPONSE_HEADER_LENGTH) { if (length < GENI_RESPONSE_HEADER_LENGTH) {
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
return; return;
} }
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) { 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(), ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
response[0], response[1], response[2], response[3], response[4]); response[0], response[1], response[2], response[3], response[4]);
return; 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)) { if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F); 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); extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) { } else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str()); ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F); 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_CURRENT_OFFSET, this->current_sensor_, 1.0F);
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_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) { if (param->open.status == ESP_GATT_OK) {
this->response_offset_ = 0; this->response_offset_ = 0;
this->response_length_ = 0; this->response_length_ = 0;
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str()); ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
} }
break; 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: { case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID); auto *chr = this->parent_->get_characteristic(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
if (chr == nullptr) { if (chr == nullptr) {
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str()); ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
break; break;
} }
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(), 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, 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); request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
void Alpha3::update() { void Alpha3::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) { if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return; return;
} }

View File

@@ -44,9 +44,11 @@ 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); auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
if (chr == nullptr) { if (chr == nullptr) {
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != 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()); ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
this->parent_->address_str().c_str());
} else { } else {
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str()); ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
this->parent_->address_str().c_str());
} }
break; break;
} }
@@ -80,7 +82,8 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
this->char_handle_, packet->length, packet->data, this->char_handle_, packet->length, packet->data,
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
} }
} }
this->current_sensor_ = 0; this->current_sensor_ = 0;
@@ -94,7 +97,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
void Am43::update() { void Am43::update() {
if (this->node_state != espbt::ClientState::ESTABLISHED) { if (this->node_state != espbt::ClientState::ESTABLISHED) {
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str()); ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
return; return;
} }
if (this->current_sensor_ == 0) { if (this->current_sensor_ == 0) {
@@ -104,7 +107,7 @@ void Am43::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, 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); packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
this->current_sensor_++; this->current_sensor_++;

View File

@@ -12,11 +12,10 @@ void AnalogThresholdBinarySensor::setup() {
// TRUE state is defined to be when sensor is >= threshold // TRUE state is defined to be when sensor is >= threshold
// so when undefined sensor value initialize to FALSE // so when undefined sensor value initialize to FALSE
if (std::isnan(sensor_value)) { if (std::isnan(sensor_value)) {
this->raw_state_ = false;
this->publish_initial_state(false); this->publish_initial_state(false);
} else { } else {
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f; this->publish_initial_state(sensor_value >=
this->publish_initial_state(this->raw_state_); (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
} }
} }
@@ -26,10 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) { this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state // if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) { if (!std::isnan(sensor_value)) {
// Use raw_state_ for hysteresis logic, not this->state which is post-filter this->publish_state(sensor_value >=
this->raw_state_ = (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
this->publish_state(this->raw_state_);
} }
}); });
} }

View File

@@ -20,7 +20,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
sensor::Sensor *sensor_{nullptr}; sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{}; TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{}; TemplatableValue<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
}; };
} // namespace analog_threshold } // namespace analog_threshold

View File

@@ -39,7 +39,7 @@ class Animation : public image::Image {
template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
public: public:
AnimationNextFrameAction(Animation *parent) : parent_(parent) {} AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->next_frame(); } void play(Ts... x) override { this->parent_->next_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -48,7 +48,7 @@ template<typename... Ts> class AnimationNextFrameAction : public Action<Ts...> {
template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> { template<typename... Ts> class AnimationPrevFrameAction : public Action<Ts...> {
public: public:
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {} AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
void play(const Ts &...x) override { this->parent_->prev_frame(); } void play(Ts... x) override { this->parent_->prev_frame(); }
protected: protected:
Animation *parent_; Animation *parent_;
@@ -58,7 +58,7 @@ template<typename... Ts> class AnimationSetFrameAction : public Action<Ts...> {
public: public:
AnimationSetFrameAction(Animation *parent) : parent_(parent) {} AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(uint16_t, frame) TEMPLATABLE_VALUE(uint16_t, frame)
void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); } void play(Ts... x) override { this->parent_->set_frame(this->frame_.value(x...)); }
protected: protected:
Animation *parent_; Animation *parent_;

View File

@@ -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_, 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); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
if (call.get_target_temperature().has_value()) { 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_, 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); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
} }
} }
@@ -124,7 +124,8 @@ 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_, 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); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
status);
} }
} }
} }
@@ -149,7 +150,7 @@ void Anova::update() {
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_, 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); pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) { if (status) {
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status); ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
} }
this->current_request_++; this->current_request_++;
} }

View File

@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
void dump_config() override; void dump_config() override;
climate::ClimateTraits traits() override { climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_supports_current_temperature(true);
traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT}); traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
traits.set_visual_min_temperature(25.0); traits.set_visual_min_temperature(25.0);
traits.set_visual_max_temperature(100.0); traits.set_visual_max_temperature(100.0);

View File

@@ -27,13 +27,12 @@ from esphome.const import (
CONF_SERVICE, CONF_SERVICE,
CONF_SERVICES, CONF_SERVICES,
CONF_TAG, CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
CONF_VARIABLES, CONF_VARIABLES,
) )
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -64,24 +63,18 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger "HomeAssistantActionResponseTrigger", automation.Trigger
) )
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition) 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) UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument") ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = { SERVICE_ARG_NATIVE_TYPES = {
"bool": cg.bool_, "bool": bool,
"int": cg.int32, "int": cg.int32,
"float": cg.float_, "float": float,
"string": cg.std_string, "string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"), "bool[]": cg.std_vector.template(bool),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"), "int[]": cg.std_vector.template(cg.int32),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"), "float[]": cg.std_vector.template(float),
"string[]": cg.FixedVector.template(cg.std_string) "string[]": cg.std_vector.template(cg.std_string),
.operator("const")
.operator("ref"),
} }
CONF_ENCRYPTION = "encryption" CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay" CONF_BATCH_DELAY = "batch_delay"
@@ -90,7 +83,6 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states" CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog" CONF_LISTEN_BACKLOG = "listen_backlog"
CONF_MAX_SEND_QUEUE = "max_send_queue" CONF_MAX_SEND_QUEUE = "max_send_queue"
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
def validate_encryption_key(value): def validate_encryption_key(value):
@@ -107,85 +99,6 @@ def validate_encryption_key(value):
return 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( ACTIONS_SCHEMA = automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
@@ -196,20 +109,10 @@ ACTIONS_SCHEMA = automation.validate_automation(
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True), 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.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION), cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_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,
),
), ),
) )
@@ -246,23 +149,12 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
_LOGGER.warning( _LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. " "API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. " "Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api/#configuration-variables" "See https://esphome.io/components/api.html#configuration-variables"
) )
return config return config
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
from esphome.components import socket
# API needs 1 listening socket + typically 3 concurrent client connections
# (not max_connections, which is the upper limit rarely reached)
sockets_needed = 1 + 3
socket.consume_sockets(sockets_needed, "api")(config)
return config
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
{ {
@@ -322,7 +214,6 @@ CONFIG_SCHEMA = cv.All(
esp32=8, # More RAM, can buffer more esp32=8, # More RAM, can buffer more
rp2040=5, # Limited RAM rp2040=5, # Limited RAM
bk72xx=8, # Moderate RAM bk72xx=8, # Moderate RAM
nrf52=8, # Moderate RAM
rtl87xx=8, # Moderate RAM rtl87xx=8, # Moderate RAM
host=16, # Abundant resources host=16, # Abundant resources
ln882x=8, # Moderate RAM ln882x=8, # Moderate RAM
@@ -331,18 +222,14 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA), ).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS), cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_api_config, _validate_api_config,
_consume_api_sockets,
) )
@coroutine_with_priority(CoroPriority.WEB) @coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None: async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT])) cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]: if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD") cg.add_define("USE_API_PASSWORD")
@@ -355,13 +242,9 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS])) cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE]) cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled # Set USE_API_SERVICES if any services are enabled
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]: if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_USER_DEFINED_ACTIONS") cg.add_define("USE_API_SERVICES")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
if config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_CUSTOM_SERVICES")
if config[CONF_HOMEASSISTANT_SERVICES]: if config[CONF_HOMEASSISTANT_SERVICES]:
cg.add_define("USE_API_HOMEASSISTANT_SERVICES") cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
@@ -370,66 +253,21 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("USE_API_HOMEASSISTANT_STATES") cg.add_define("USE_API_HOMEASSISTANT_STATES")
if actions := config.get(CONF_ACTIONS, []): if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = []
for conf in actions: for conf in actions:
func_args: list[tuple[MockObj, str]] = [] template_args = []
service_template_args: list[MockObj] = [] # User service argument types func_args = []
service_arg_names = []
# 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(): for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_] native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native) template_args.append(native)
func_args.append((native, name)) func_args.append((native, name))
service_arg_names.append(name) service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types templ = cg.TemplateArguments(*template_args)
templ = cg.TemplateArguments(supports_response, *service_template_args)
trigger = cg.new_Pvariable( trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
templ,
conf[CONF_ACTION],
service_arg_names,
) )
triggers.append(trigger) cg.add(var.register_user_service(trigger))
auto = await automation.build_automation(trigger, func_args, conf) 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))
if CONF_ON_CLIENT_CONNECTED in config: if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER") cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
@@ -673,98 +511,9 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var return var
CONF_SUCCESS = "success" @automation.register_condition("api.connected", APIConnectedCondition, {})
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): async def api_connected_to_code(config, condition_id, template_arg, args):
var = cg.new_Pvariable(condition_id, template_arg) return 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]: def FILTER_SOURCE_FILES() -> list[str]:

View File

@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9; bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11; EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
} }
// Deprecated in API version 1.6 - only used in deprecated fields // Deprecated in API version 1.6 - only used in deprecated fields
@@ -477,7 +477,7 @@ message FanCommandRequest {
bool has_speed_level = 10; bool has_speed_level = 10;
int32 speed_level = 11; int32 speed_level = 11;
bool has_preset_mode = 12; bool has_preset_mode = 12;
string preset_mode = 13 [(pointer_to_buffer) = true]; string preset_mode = 13;
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
} }
@@ -518,7 +518,7 @@ message ListEntitiesLightResponse {
bool legacy_supports_color_temperature = 8 [deprecated=true]; bool legacy_supports_color_temperature = 8 [deprecated=true];
float min_mireds = 9; float min_mireds = 9;
float max_mireds = 10; float max_mireds = 10;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string effects = 11;
bool disabled_by_default = 13; bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 15; EntityCategory entity_category = 15;
@@ -579,7 +579,7 @@ message LightCommandRequest {
bool has_flash_length = 16; bool has_flash_length = 16;
uint32 flash_length = 17; uint32 flash_length = 17;
bool has_effect = 18; bool has_effect = 18;
string effect = 19 [(pointer_to_buffer) = true]; string effect = 19;
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
} }
@@ -589,7 +589,6 @@ enum SensorStateClass {
STATE_CLASS_MEASUREMENT = 1; STATE_CLASS_MEASUREMENT = 1;
STATE_CLASS_TOTAL_INCREASING = 2; STATE_CLASS_TOTAL_INCREASING = 2;
STATE_CLASS_TOTAL = 3; STATE_CLASS_TOTAL = 3;
STATE_CLASS_MEASUREMENT_ANGLE = 4;
} }
// Deprecated in API version 1.5 // Deprecated in API version 1.5
@@ -747,7 +746,7 @@ message NoiseEncryptionSetKeyRequest {
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_API_NOISE"; option (ifdef) = "USE_API_NOISE";
bytes key = 1 [(pointer_to_buffer) = true]; bytes key = 1;
} }
message NoiseEncryptionSetKeyResponse { message NoiseEncryptionSetKeyResponse {
@@ -824,9 +823,9 @@ message HomeAssistantStateResponse {
option (no_delay) = true; option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_STATES"; option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1 [(pointer_to_buffer) = true]; string entity_id = 1;
string state = 2 [(pointer_to_buffer) = true]; string state = 2;
string attribute = 3 [(pointer_to_buffer) = true]; string attribute = 3;
} }
// ==================== IMPORT TIME ==================== // ==================== IMPORT TIME ====================
@@ -855,31 +854,22 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
SERVICE_ARG_TYPE_STRING_ARRAY = 7; 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 { message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
ServiceArgType type = 2; ServiceArgType type = 2;
} }
message ListEntitiesServicesResponse { message ListEntitiesServicesResponse {
option (id) = 41; option (id) = 41;
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
string name = 1; string name = 1;
fixed32 key = 2; fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true]; repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
} }
message ExecuteServiceArgument { message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
bool bool_ = 1; bool bool_ = 1;
int32 legacy_int = 2; int32 legacy_int = 2;
float float_ = 3; float float_ = 3;
@@ -895,25 +885,10 @@ message ExecuteServiceRequest {
option (id) = 42; option (id) = 42;
option (source) = SOURCE_CLIENT; option (source) = SOURCE_CLIENT;
option (no_delay) = true; option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS"; option (ifdef) = "USE_API_SERVICES";
fixed32 key = 1; fixed32 key = 1;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true]; 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 ==================== // ==================== CAMERA ====================
@@ -1014,7 +989,7 @@ message ListEntitiesClimateResponse {
bool supports_current_temperature = 5; // Deprecated: use feature_flags bool supports_current_temperature = 5; // Deprecated: use feature_flags
bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"]; repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
float visual_min_temperature = 8; float visual_min_temperature = 8;
float visual_max_temperature = 9; float visual_max_temperature = 9;
float visual_target_temperature_step = 10; float visual_target_temperature_step = 10;
@@ -1023,11 +998,11 @@ message ListEntitiesClimateResponse {
// Deprecated in API version 1.5 // Deprecated in API version 1.5
bool legacy_supports_away = 11 [deprecated=true]; bool legacy_supports_away = 11 [deprecated=true];
bool supports_action = 12; // Deprecated: use feature_flags bool supports_action = 12; // Deprecated: use feature_flags
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
bool disabled_by_default = 18; bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 20; EntityCategory entity_category = 20;
@@ -1091,95 +1066,16 @@ message ClimateCommandRequest {
bool has_swing_mode = 14; bool has_swing_mode = 14;
ClimateSwingMode swing_mode = 15; ClimateSwingMode swing_mode = 15;
bool has_custom_fan_mode = 16; bool has_custom_fan_mode = 16;
string custom_fan_mode = 17 [(pointer_to_buffer) = true]; string custom_fan_mode = 17;
bool has_preset = 18; bool has_preset = 18;
ClimatePreset preset = 19; ClimatePreset preset = 19;
bool has_custom_preset = 20; bool has_custom_preset = 20;
string custom_preset = 21 [(pointer_to_buffer) = true]; string custom_preset = 21;
bool has_target_humidity = 22; bool has_target_humidity = 22;
float target_humidity = 23; float target_humidity = 23;
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
} }
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ==================== // ==================== NUMBER ====================
enum NumberMode { enum NumberMode {
NUMBER_MODE_AUTO = 0; NUMBER_MODE_AUTO = 0;
@@ -1247,7 +1143,7 @@ message ListEntitiesSelectResponse {
reserved 4; // Deprecated: was string unique_id reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string options = 6 [(container_pointer) = "std::vector"];
bool disabled_by_default = 7; bool disabled_by_default = 7;
EntityCategory entity_category = 8; EntityCategory entity_category = 8;
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
@@ -1274,7 +1170,7 @@ message SelectCommandRequest {
option (base_class) = "CommandProtoMessage"; option (base_class) = "CommandProtoMessage";
fixed32 key = 1; fixed32 key = 1;
string state = 2 [(pointer_to_buffer) = true]; string state = 2;
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
} }
@@ -2251,7 +2147,7 @@ message ListEntitiesEventResponse {
EntityCategory entity_category = 7; EntityCategory entity_category = 7;
string device_class = 8; string device_class = 8;
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"]; repeated string event_types = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"]; uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
} }
message EventResponse { message EventResponse {

View File

@@ -6,18 +6,11 @@
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
#include "api_frame_helper_plaintext.h" #include "api_frame_helper_plaintext.h"
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
#include <cerrno> #include <cerrno>
#include <cinttypes> #include <cinttypes>
#include <functional> #include <functional>
#include <limits> #include <limits>
#include <new>
#include <utility> #include <utility>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/entity_base.h" #include "esphome/core/entity_base.h"
@@ -43,9 +36,6 @@
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
#include "esphome/components/zwave_proxy/zwave_proxy.h" #include "esphome/components/zwave_proxy/zwave_proxy.h"
#endif #endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
namespace esphome::api { namespace esphome::api {
@@ -97,10 +87,11 @@ static const int CAMERA_STOP_STREAM = 5000;
return; return;
#endif // USE_DEVICES #endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) { 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) #if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
auto &noise_ctx = parent->get_noise_ctx(); auto noise_ctx = parent->get_noise_ctx();
if (noise_ctx.has_psk()) { if (noise_ctx->has_psk()) {
this->helper_ = this->helper_ =
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)}; std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
} else { } else {
@@ -131,14 +122,11 @@ void APIConnection::start() {
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err); this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
return; return;
} }
// Initialize client name with peername (IP address) until Hello message provides actual name this->client_info_.peername = helper_->getpeername();
char peername[socket::PEERNAME_MAX_LEN]; this->client_info_.name = this->client_info_.peername;
this->helper_->getpeername_to(peername);
this->client_info_.name = peername;
} }
APIConnection::~APIConnection() { APIConnection::~APIConnection() {
this->destroy_active_iterator_();
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this);
@@ -151,32 +139,6 @@ APIConnection::~APIConnection() {
#endif #endif
} }
void APIConnection::destroy_active_iterator_() {
switch (this->active_iterator_) {
case ActiveIterator::LIST_ENTITIES:
this->iterator_storage_.list_entities.~ListEntitiesIterator();
break;
case ActiveIterator::INITIAL_STATE:
this->iterator_storage_.initial_state.~InitialStateIterator();
break;
case ActiveIterator::NONE:
break;
}
this->active_iterator_ = ActiveIterator::NONE;
}
void APIConnection::begin_iterator_(ActiveIterator type) {
this->destroy_active_iterator_();
this->active_iterator_ = type;
if (type == ActiveIterator::LIST_ENTITIES) {
new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
this->iterator_storage_.list_entities.begin();
} else {
new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
this->iterator_storage_.initial_state.begin();
}
}
void APIConnection::loop() { void APIConnection::loop() {
if (this->flags_.next_close) { if (this->flags_.next_close) {
// requested a disconnect // requested a disconnect
@@ -207,7 +169,8 @@ void APIConnection::loop() {
} else { } else {
this->last_traffic_ = now; this->last_traffic_ = now;
// read a packet // read a packet
this->read_message(buffer.data_len, buffer.type, buffer.data); this->read_message(buffer.data_len, buffer.type,
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
if (this->flags_.remove) if (this->flags_.remove)
return; return;
} }
@@ -219,42 +182,28 @@ void APIConnection::loop() {
this->process_batch_(); this->process_batch_();
} }
switch (this->active_iterator_) { if (!this->list_entities_iterator_.completed()) {
case ActiveIterator::LIST_ENTITIES: this->process_iterator_batch_(this->list_entities_iterator_);
if (this->iterator_storage_.list_entities.completed()) { } else if (!this->initial_state_iterator_.completed()) {
this->destroy_active_iterator_(); this->process_iterator_batch_(this->initial_state_iterator_);
if (this->flags_.state_subscription) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE); // If we've completed initial states, process any remaining and clear the flag
} if (this->initial_state_iterator_.completed()) {
} else { // Process any remaining batched messages immediately
this->process_iterator_batch_(this->iterator_storage_.list_entities); if (!this->deferred_batch_.empty()) {
this->process_batch_();
} }
break; // Now that everything is sent, enable immediate sending for future state changes
case ActiveIterator::INITIAL_STATE: this->flags_.should_try_send_immediately = true;
if (this->iterator_storage_.initial_state.completed()) { }
this->destroy_active_iterator_();
// Process any remaining batched messages immediately
if (!this->deferred_batch_.empty()) {
this->process_batch_();
}
// 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();
} else {
this->process_iterator_batch_(this->iterator_storage_.initial_state);
}
break;
case ActiveIterator::NONE:
break;
} }
if (this->flags_.sent_ping) { if (this->flags_.sent_ping) {
// Disconnect if not responded within 2.5*keepalive // Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) { if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
on_fatal_error(); on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting")); ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
this->client_info_.peername.c_str());
} }
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) { } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
// Only send ping if we're not disconnecting // Only send ping if we're not disconnecting
@@ -271,24 +220,40 @@ void APIConnection::loop() {
} }
} }
#ifdef USE_CAMERA
if (this->image_reader_ && this->image_reader_->available() && this->helper_->can_write_without_blocking()) {
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
}
}
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
if (state_subs_at_ >= 0) { if (state_subs_at_ >= 0) {
this->process_state_subscriptions_(); this->process_state_subscriptions_();
} }
#endif #endif
#ifdef USE_CAMERA
// Process camera last - state updates are higher priority
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
} }
bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) { bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
// remote initiated disconnect_client // remote initiated disconnect_client
// don't close yet, we still need to send the disconnect response // don't close yet, we still need to send the disconnect response
// close will happen on next loop // close will happen on next loop
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected")); ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
this->flags_.next_close = true; this->flags_.next_close = true;
DisconnectResponse resp; DisconnectResponse resp;
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE); return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
@@ -445,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
} }
if (traits.supports_direction()) if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes() && fan->has_preset_mode()) if (traits.supports_preset_modes())
msg.set_preset_mode(StringRef(fan->get_preset_mode())); msg.set_preset_mode(StringRef(fan->preset_mode));
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -458,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
msg.supported_preset_modes = &traits.supported_preset_modes(); msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::fan_command(const FanCommandRequest &msg) { void APIConnection::fan_command(const FanCommandRequest &msg) {
@@ -474,7 +439,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
if (msg.has_direction) if (msg.has_direction)
call.set_direction(static_cast<fan::FanDirection>(msg.direction)); call.set_direction(static_cast<fan::FanDirection>(msg.direction));
if (msg.has_preset_mode) if (msg.has_preset_mode)
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len); call.set_preset_mode(msg.preset_mode);
call.perform(); call.perform();
} }
#endif #endif
@@ -511,24 +476,19 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
auto *light = static_cast<light::LightState *>(entity); auto *light = static_cast<light::LightState *>(entity);
ListEntitiesLightResponse msg; ListEntitiesLightResponse msg;
auto traits = light->get_traits(); auto traits = light->get_traits();
auto supported_modes = traits.get_supported_color_modes();
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
msg.supported_color_modes = &supported_modes; msg.supported_color_modes = &traits.get_supported_color_modes();
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) { traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
msg.min_mireds = traits.get_min_mireds(); msg.min_mireds = traits.get_min_mireds();
msg.max_mireds = traits.get_max_mireds(); msg.max_mireds = traits.get_max_mireds();
} }
FixedVector<const char *> effects_list;
if (light->supports_effects()) { if (light->supports_effects()) {
auto &light_effects = light->get_effects(); msg.effects.emplace_back("None");
effects_list.init(light_effects.size() + 1); for (auto *effect : light->get_effects()) {
effects_list.push_back("None"); msg.effects.push_back(effect->get_name());
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, return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -560,7 +520,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
if (msg.has_flash_length) if (msg.has_flash_length)
call.set_flash_length(msg.flash_length); call.set_flash_length(msg.flash_length);
if (msg.has_effect) if (msg.has_effect)
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len); call.set_effect(msg.effect);
call.perform(); call.perform();
} }
#endif #endif
@@ -677,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
} }
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
} }
if (traits.get_supports_presets() && climate->preset.has_value()) { if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
} }
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
resp.set_custom_preset(StringRef(climate->get_custom_preset())); resp.set_custom_preset(StringRef(climate->custom_preset.value()));
} }
if (traits.get_supports_swing_modes()) if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@@ -709,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION); msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
// Current feature flags and other supported parameters // Current feature flags and other supported parameters
msg.feature_flags = traits.get_feature_flags(); msg.feature_flags = traits.get_feature_flags();
msg.supported_modes = &traits.get_supported_modes(); msg.supported_modes = &traits.get_supported_modes_for_api_();
msg.visual_min_temperature = traits.get_visual_min_temperature(); msg.visual_min_temperature = traits.get_visual_min_temperature();
msg.visual_max_temperature = traits.get_visual_max_temperature(); msg.visual_max_temperature = traits.get_visual_max_temperature();
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step(); msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
msg.visual_current_temperature_step = traits.get_visual_current_temperature_step(); msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
msg.visual_min_humidity = traits.get_visual_min_humidity(); msg.visual_min_humidity = traits.get_visual_min_humidity();
msg.visual_max_humidity = traits.get_visual_max_humidity(); msg.visual_max_humidity = traits.get_visual_max_humidity();
msg.supported_fan_modes = &traits.get_supported_fan_modes(); msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes(); msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
msg.supported_presets = &traits.get_supported_presets(); msg.supported_presets = &traits.get_supported_presets_for_api_();
msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
msg.supported_swing_modes = &traits.get_supported_swing_modes(); msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -739,11 +699,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
if (msg.has_fan_mode) if (msg.has_fan_mode)
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
if (msg.has_custom_fan_mode) if (msg.has_custom_fan_mode)
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len); call.set_fan_mode(msg.custom_fan_mode);
if (msg.has_preset) if (msg.has_preset)
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset)); call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
if (msg.has_custom_preset) if (msg.has_custom_preset)
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len); call.set_preset(msg.custom_preset);
if (msg.has_swing_mode) if (msg.has_swing_mode)
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode)); call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
call.perform(); call.perform();
@@ -917,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp; SelectStateResponse resp;
resp.set_state(StringRef(select->current_option())); resp.set_state(StringRef(select->state));
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@@ -932,7 +892,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
} }
void APIConnection::select_command(const SelectCommandRequest &msg) { void APIConnection::select_command(const SelectCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len); call.set_option(msg.state);
call.perform(); call.perform();
} }
#endif #endif
@@ -1084,36 +1044,6 @@ void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) {
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
void APIConnection::try_send_camera_image_() {
if (!this->image_reader_)
return;
// Send as many chunks as possible without blocking
while (this->image_reader_->available()) {
if (!this->helper_->can_write_without_blocking())
return;
uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
bool done = this->image_reader_->available() == to_send;
CameraImageResponse msg;
msg.key = camera::Camera::instance()->get_object_id_hash();
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
msg.done = done;
#ifdef USE_DEVICES
msg.device_id = camera::Camera::instance()->get_device_id();
#endif
if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) {
return; // Send failed, try again later
}
this->image_reader_->consume_data(to_send);
if (done) {
this->image_reader_->return_image();
return;
}
}
}
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) { void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
if (!this->flags_.state_subscription) if (!this->flags_.state_subscription)
return; return;
@@ -1121,11 +1051,8 @@ void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image)
return; return;
if (this->image_reader_->available()) if (this->image_reader_->available())
return; return;
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) { if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE))
this->image_reader_->set_image(std::move(image)); this->image_reader_->set_image(std::move(image));
// Try to send immediately to reduce latency
this->try_send_camera_image_();
}
} }
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) { bool is_single) {
@@ -1366,63 +1293,12 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
} }
#endif #endif
#ifdef USE_WATER_HEATER
bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
return this->send_message_smart_(water_heater, &APIConnection::try_send_water_heater_state,
WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
WaterHeaterStateResponse resp;
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
resp.current_temperature = wh->get_current_temperature();
resp.target_temperature = wh->get_target_temperature();
resp.target_temperature_low = wh->get_target_temperature_low();
resp.target_temperature_high = wh->get_target_temperature_high();
resp.state = wh->get_state();
resp.key = wh->get_object_id_hash();
return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single) {
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
ListEntitiesWaterHeaterResponse msg;
auto traits = wh->get_traits();
msg.min_temperature = traits.get_min_temperature();
msg.max_temperature = traits.get_max_temperature();
msg.target_temperature_step = traits.get_target_temperature_step();
msg.supported_modes = &traits.get_supported_modes();
msg.supported_features = traits.get_feature_flags();
return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size,
is_single);
}
void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
call.set_target_temperature(msg.target_temperature);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
call.set_target_temperature_low(msg.target_temperature_low);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
call.set_target_temperature_high(msg.target_temperature_high);
if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) {
call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
}
call.perform();
}
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const char *event_type) { void APIConnection::send_event(event::Event *event, const std::string &event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE, this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE); EventResponse::ESTIMATED_SIZE);
} }
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
EventResponse resp; EventResponse resp;
resp.set_event_type(StringRef(event_type)); resp.set_event_type(StringRef(event_type));
@@ -1434,7 +1310,8 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
auto *event = static_cast<event::Event *>(entity); auto *event = static_cast<event::Event *>(entity);
ListEntitiesEventResponse msg; ListEntitiesEventResponse msg;
msg.set_device_class(event->get_device_class_ref()); msg.set_device_class(event->get_device_class_ref());
msg.event_types = &event->get_event_types(); for (const auto &event_type : event->get_event_types())
msg.event_types.push_back(event_type);
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
is_single); is_single);
} }
@@ -1506,10 +1383,9 @@ void APIConnection::complete_authentication_() {
} }
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED); this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected")); ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
// Trigger expects std::string, get fresh peername from socket this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->helper_->getpeername());
#endif #endif
#ifdef USE_HOMEASSISTANT_TIME #ifdef USE_HOMEASSISTANT_TIME
if (homeassistant::global_homeassistant_time != nullptr) { if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1525,12 +1401,11 @@ void APIConnection::complete_authentication_() {
bool APIConnection::send_hello_response(const HelloRequest &msg) { bool APIConnection::send_hello_response(const HelloRequest &msg) {
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len); this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
this->client_info_.peername = this->helper_->getpeername();
this->client_api_version_major_ = msg.api_version_major; this->client_api_version_major_ = msg.api_version_major;
this->client_api_version_minor_ = msg.api_version_minor; this->client_api_version_minor_ = msg.api_version_minor;
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(), ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
peername, this->client_api_version_major_, this->client_api_version_minor_); this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
HelloResponse resp; HelloResponse resp;
resp.api_version_major = 1; resp.api_version_major = 1;
@@ -1576,86 +1451,48 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_AREAS #ifdef USE_AREAS
resp.set_suggested_area(StringRef(App.get_area())); resp.set_suggested_area(StringRef(App.get_area()));
#endif #endif
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) // mac_address must store temporary string - will be valid during send_message call
char mac_address[18]; std::string mac_address = get_mac_address_pretty();
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_mac_address(StringRef(mac_address));
resp.set_esphome_version(ESPHOME_VERSION_REF); resp.set_esphome_version(ESPHOME_VERSION_REF);
// Stack buffer for build time string resp.set_compilation_time(App.get_compilation_time_ref());
char build_time_str[Application::BUILD_TIME_STR_SIZE];
App.get_build_time_string(build_time_str);
resp.set_compilation_time(StringRef(build_time_str));
// Manufacturer string - define once, handle ESP8266 PROGMEM separately // Compile-time StringRef constants for manufacturers
#if defined(USE_ESP8266) || defined(USE_ESP32) #if defined(USE_ESP8266) || defined(USE_ESP32)
#define ESPHOME_MANUFACTURER "Espressif" static constexpr auto MANUFACTURER = StringRef::from_lit("Espressif");
#elif defined(USE_RP2040) #elif defined(USE_RP2040)
#define ESPHOME_MANUFACTURER "Raspberry Pi" static constexpr auto MANUFACTURER = StringRef::from_lit("Raspberry Pi");
#elif defined(USE_BK72XX) #elif defined(USE_BK72XX)
#define ESPHOME_MANUFACTURER "Beken" static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
#elif defined(USE_LN882X) #elif defined(USE_LN882X)
#define ESPHOME_MANUFACTURER "Lightning" static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
#elif defined(USE_NRF52)
#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
#elif defined(USE_RTL87XX) #elif defined(USE_RTL87XX)
#define ESPHOME_MANUFACTURER "Realtek" static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
#elif defined(USE_HOST) #elif defined(USE_HOST)
#define ESPHOME_MANUFACTURER "Host" static constexpr auto MANUFACTURER = StringRef::from_lit("Host");
#endif #endif
#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); 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); static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
resp.set_model(MODEL); resp.set_model(MODEL);
#endif
#ifdef USE_DEEP_SLEEP #ifdef USE_DEEP_SLEEP
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
#endif #endif
#ifdef ESPHOME_PROJECT_NAME #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_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION); static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
resp.set_project_name(PROJECT_NAME); resp.set_project_name(PROJECT_NAME);
resp.set_project_version(PROJECT_VERSION); resp.set_project_version(PROJECT_VERSION);
#endif #endif
#endif
#ifdef USE_WEBSERVER #ifdef USE_WEBSERVER
resp.webserver_port = USE_WEBSERVER_PORT; resp.webserver_port = USE_WEBSERVER_PORT;
#endif #endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes) // bt_mac must store temporary string - will be valid during send_message call
char bluetooth_mac[18]; std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac)); resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
#endif #endif
#ifdef USE_VOICE_ASSISTANT #ifdef USE_VOICE_ASSISTANT
@@ -1695,83 +1532,25 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) { void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
// Skip if entity_id is empty (invalid message)
if (msg.entity_id_len == 0) {
return;
}
for (auto &it : this->parent_->get_state_subs()) { for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id: check length matches and content matches if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
size_t entity_id_len = strlen(it.entity_id); it.callback(msg.state);
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
continue;
} }
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute_len ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
continue;
}
// Create temporary string for callback (callback takes const std::string &)
// Handle empty state (nullptr with len=0)
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
it.callback(state);
} }
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void APIConnection::execute_service(const ExecuteServiceRequest &msg) { void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
bool found = false; 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()) { for (auto *service : this->parent_->get_user_services()) {
if (service->execute_service(msg)) { if (service->execute_service(msg)) {
found = true; found = true;
} }
} }
#endif
if (!found) { if (!found) {
ESP_LOGV(TAG, "Could not find service"); 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 #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -1793,13 +1572,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
resp.success = false; resp.success = false;
psk_t psk{}; psk_t psk{};
if (msg.key_len == 0) { if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
if (this->parent_->clear_noise_psk(true)) {
resp.success = true;
} else {
ESP_LOGW(TAG, "Failed to clear encryption key");
}
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
ESP_LOGW(TAG, "Invalid encryption key length"); ESP_LOGW(TAG, "Invalid encryption key length");
} else if (!this->parent_->save_noise_psk(psk, true)) { } else if (!this->parent_->save_noise_psk(psk, true)) {
ESP_LOGW(TAG, "Failed to save encryption key"); ESP_LOGW(TAG, "Failed to save encryption key");
@@ -1851,12 +1624,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
#ifdef USE_API_PASSWORD #ifdef USE_API_PASSWORD
void APIConnection::on_unauthenticated_access() { void APIConnection::on_unauthenticated_access() {
this->on_fatal_error(); this->on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no authentication")); ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
} }
#endif #endif
void APIConnection::on_no_setup_connection() { void APIConnection::on_no_setup_connection() {
this->on_fatal_error(); this->on_fatal_error();
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup")); ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
} }
void APIConnection::on_fatal_error() { void APIConnection::on_fatal_error() {
this->helper_->close(); this->helper_->close();
@@ -1870,14 +1643,16 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
// O(n) but optimized for RAM and not performance. // O(n) but optimized for RAM and not performance.
for (auto &item : items) { for (auto &item : items) {
if (item.entity == entity && item.message_type == message_type) { if (item.entity == entity && item.message_type == message_type) {
// Replace with new creator // Clean up old creator before replacing
item.creator = creator; item.creator.cleanup(message_type);
// Move assign the new creator
item.creator = std::move(creator);
return; return;
} }
} }
// No existing item found, add new one // No existing item found, add new one
items.emplace_back(entity, creator, message_type, estimated_size); items.emplace_back(entity, std::move(creator), message_type, estimated_size);
} }
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type,
@@ -1886,7 +1661,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, MessageCre
// This avoids expensive vector::insert which shifts all elements // This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect) // 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 // If we're disconnecting, pings are blocked, so this simple swap is sufficient
items.emplace_back(entity, creator, message_type, estimated_size); items.emplace_back(entity, std::move(creator), message_type, estimated_size);
if (items.size() > 1) { if (items.size() > 1) {
// Swap the new high-priority item to the front // Swap the new high-priority item to the front
std::swap(items.front(), items.back()); std::swap(items.front(), items.back());
@@ -2040,7 +1815,7 @@ void APIConnection::process_batch_() {
// Handle remaining items more efficiently // Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) { if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning // Remove processed items from the beginning with proper cleanup
this->deferred_batch_.remove_front(items_processed); this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items // Reschedule for remaining items
this->schedule_batch_(); this->schedule_batch_();
@@ -2053,10 +1828,10 @@ void APIConnection::process_batch_() {
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single, uint8_t message_type) const { bool is_single, uint8_t message_type) const {
#ifdef USE_EVENT #ifdef USE_EVENT
// Special case: EventResponse uses const char * pointer // Special case: EventResponse uses string pointer
if (message_type == EventResponse::MESSAGE_TYPE) { if (message_type == EventResponse::MESSAGE_TYPE) {
auto *e = static_cast<event::Event *>(entity); auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single); return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
} }
#endif #endif
@@ -2094,8 +1869,8 @@ void APIConnection::process_state_subscriptions_() {
SubscribeHomeAssistantStateResponse resp; SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id)); resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by using the const char* pointer if it exists // Avoid string copy by directly using the optional's value if it exists
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef("")); resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
resp.once = it.once; resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) { if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
@@ -2104,18 +1879,9 @@ void APIConnection::process_state_subscriptions_() {
} }
#endif // USE_API_HOMEASSISTANT_STATES #endif // USE_API_HOMEASSISTANT_STATES
void APIConnection::log_client_(int level, const LogString *message) {
char peername[socket::PEERNAME_MAX_LEN];
this->helper_->getpeername_to(peername);
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->client_info_.name.c_str(), peername,
LOG_STR_ARG(message));
}
void APIConnection::log_warning_(const LogString *message, APIError err) { void APIConnection::log_warning_(const LogString *message, APIError err) {
char peername[socket::PEERNAME_MAX_LEN]; ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
this->helper_->getpeername_to(peername); LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), peername, LOG_STR_ARG(message),
LOG_STR_ARG(api_error_to_logstr(err)), errno);
} }
} // namespace esphome::api } // namespace esphome::api

View File

@@ -17,9 +17,8 @@ namespace esphome::api {
// Client information structure // Client information structure
struct ClientInfo { struct ClientInfo {
std::string name; // Client name from Hello message std::string name; // Client name from Hello message
// Note: peername (IP address) is not stored here to save memory. std::string peername; // IP:port from socket
// Use helper_->getpeername_to() or helper_->getpeername() when needed.
}; };
// Keepalive timeout in milliseconds // Keepalive timeout in milliseconds
@@ -177,13 +176,8 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type); void send_event(event::Event *event, const std::string &event_type);
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
@@ -209,14 +203,10 @@ class APIConnection final : public APIServerConnection {
bool send_disconnect_response(const DisconnectRequest &msg) override; bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override; bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override; bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); } void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override { void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true; this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active this->initial_state_iterator_.begin();
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
} }
void subscribe_logs(const SubscribeLogsRequest &msg) override { void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level; this->flags_.log_subscription = msg.level;
@@ -231,15 +221,8 @@ class APIConnection final : public APIServerConnection {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override; void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void execute_service(const ExecuteServiceRequest &msg) override; 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 #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override; bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
@@ -291,21 +274,12 @@ class APIConnection final : public APIServerConnection {
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const std::string &get_name() const { return this->client_info_.name; } const std::string &get_name() const { return this->client_info_.name; }
/// Get peer name (IP address) into a stack buffer - avoids heap allocation const std::string &get_peername() const { return this->client_info_.peername; }
size_t get_peername_to(std::span<char, socket::PEERNAME_MAX_LEN> buf) const {
return this->helper_->getpeername_to(buf);
}
/// Get peer name as std::string - use sparingly, allocates on heap
std::string get_peername() const { return this->helper_->getpeername(); }
protected: protected:
// Helper function to handle authentication completion // Helper function to handle authentication completion
void complete_authentication_(); void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_(); void process_state_subscriptions_();
#endif #endif
@@ -329,10 +303,17 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) { APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash(); msg.key = entity->get_object_id_hash();
// Get object_id with zero heap allocation // Try to use static reference first to avoid allocation
// Static case returns direct reference, dynamic case uses buffer StringRef static_ref = entity->get_object_id_ref_for_api_();
char object_id_buf[OBJECT_ID_MAX_LEN]; // Store dynamic string outside the if-else to maintain lifetime
msg.set_object_id(entity->get_object_id_to(object_id_buf)); std::string object_id;
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
if (entity->has_own_name()) { if (entity->has_own_name()) {
msg.set_name(entity->get_name()); msg.set_name(entity->get_name());
@@ -468,14 +449,8 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single); bool is_single);
#endif #endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn, static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
#endif #endif
@@ -508,22 +483,10 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_; std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_; APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators) // Group 2: Larger objects (must be 4-byte aligned)
// These iterators are never active simultaneously - list_entities runs to completion // These contain vectors/pointers internally, so putting them early ensures good alignment
// before initial_state begins, so we use a union with explicit construction/destruction. InitialStateIterator initial_state_iterator_;
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE }; ListEntitiesIterator list_entities_iterator_;
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
#ifdef USE_CAMERA #ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_; std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif #endif
@@ -542,18 +505,51 @@ class APIConnection final : public APIServerConnection {
class MessageCreator { class MessageCreator {
public: public:
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; } MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Constructor for string state capture
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
// No destructor - cleanup must be called explicitly with message_type
// 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) {
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
// In our usage, this happens in add_item() deduplication and vector::erase()
data_ = other.data_;
other.data_.function_ptr = nullptr;
}
return *this;
}
// Call operator - uses message_type to determine union type // Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single, uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const; uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint8_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
data_.string_ptr = nullptr;
}
#endif
}
private: private:
union Data { union Data {
MessageCreatorPtr function_ptr; MessageCreatorPtr function_ptr;
const char *const_char_ptr; std::string *string_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit } data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
}; };
// Generic batching mechanism for both state updates and entity info // Generic batching mechanism for both state updates and entity info
@@ -566,41 +562,52 @@ class APIConnection final : public APIServerConnection {
// Constructor for creating BatchItem // Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {} : entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
}; };
std::vector<BatchItem> items; std::vector<BatchItem> items;
uint32_t batch_start_time{0}; uint32_t batch_start_time{0};
// No pre-allocation - log connections never use batching, and for private:
// connections that do, buffers are released after initial sync anyway // Helper to cleanup items from the beginning
void cleanup_items_(size_t count) {
for (size_t i = 0; i < count; i++) {
items[i].creator.cleanup(items[i].message_type);
}
}
public:
DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8);
}
~DeferredBatch() {
// Ensure cleanup of any remaining items
clear();
}
// Add item to the batch // Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping) // Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size); void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items // Clear all items with proper cleanup
void clear() { void clear() {
cleanup_items_(items.size());
items.clear(); items.clear();
batch_start_time = 0; batch_start_time = 0;
} }
// Remove processed items from the front // Remove processed items from the front with proper cleanup
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); } void remove_front(size_t count) {
cleanup_items_(count);
items.erase(items.begin(), items.begin() + count);
}
bool empty() const { return items.empty(); } bool empty() const { return items.empty(); }
size_t size() const { return items.size(); } size_t size() const { return items.size(); }
const BatchItem &operator[](size_t index) const { return items[index]; } 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) // DeferredBatch here (16 bytes, 4-byte aligned)
@@ -638,9 +645,7 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them) // 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0}; uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0}; uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding // Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const; uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical // Message will use 8 more bytes than the minimum size, and typical
@@ -677,30 +682,21 @@ class APIConnection final : public APIServerConnection {
} }
#endif #endif
// Helper to check if a message type should bypass batching
// Returns true if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
// AND batch_delay = 0)
inline bool should_send_immediately_(uint8_t message_type) const {
return (
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
#ifdef USE_EVENT
message_type == EventResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
}
// Helper method to send a message either immediately or via batching // Helper method to send a message either immediately or via batching
// Tries immediate send if should_send_immediately_() returns true and buffer has space
// Falls back to batching if immediate send fails or isn't applicable
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) { uint8_t estimated_size) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { // Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
// AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
this->helper_->can_write_without_blocking()) {
// Now actually encode and send // Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
@@ -718,30 +714,9 @@ class APIConnection final : public APIServerConnection {
return this->schedule_message_(entity, creator, message_type, estimated_size); return this->schedule_message_(entity, creator, message_type, estimated_size);
} }
// Overload for MessageCreator (used by events which need to capture event_type)
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
// Try to send immediately if message type should bypass batching and buffer has space
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, creator, message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type // 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) { bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size); this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_(); return this->schedule_batch_();
} }
@@ -758,8 +733,6 @@ class APIConnection final : public APIServerConnection {
return this->schedule_batch_(); return this->schedule_batch_();
} }
// Helper function to log client messages with name and peername
void log_client_(int level, const LogString *message);
// Helper function to log API errors with errno // Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err); void log_warning_(const LogString *message, APIError err);
// Helper to handle fatal errors with logging // Helper to handle fatal errors with logging

View File

@@ -13,16 +13,8 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper"; static const char *const TAG = "api.frame_helper";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) \
do { \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())

View File

@@ -35,9 +35,10 @@ struct ClientInfo;
class ProtoWriteBuffer; class ProtoWriteBuffer;
struct ReadPacketBuffer { struct ReadPacketBuffer {
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call) std::vector<uint8_t> container;
uint16_t data_len;
uint16_t type; uint16_t type;
uint16_t data_offset;
uint16_t data_len;
}; };
// Packed packet info structure to minimize memory usage // Packed packet info structure to minimize memory usage
@@ -83,7 +84,9 @@ class APIFrameHelper {
public: public:
APIFrameHelper() = default; APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info) explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_(std::move(socket)), client_info_(client_info) {} : socket_owned_(std::move(socket)), client_info_(client_info) {
socket_ = socket_owned_.get();
}
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop(); virtual APIError loop();
@@ -91,7 +94,6 @@ class APIFrameHelper {
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; } bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); } std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); } int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
size_t getpeername_to(std::span<char, socket::PEERNAME_MAX_LEN> buf) { return socket_->getpeername_to(buf); }
APIError close() { APIError close() {
state_ = State::CLOSED; state_ = State::CLOSED;
int err = this->socket_->close(); int err = this->socket_->close();
@@ -119,22 +121,6 @@ class APIFrameHelper {
uint8_t frame_footer_size() const { return frame_footer_size_; } uint8_t frame_footer_size() const { return frame_footer_size_; }
// Check if socket has data ready to read // Check if socket has data ready to read
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); } 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: protected:
// Buffer containing data to be sent // Buffer containing data to be sent
@@ -163,8 +149,9 @@ class APIFrameHelper {
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, 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); const std::string &info, StateEnum &state, StateEnum failed_state);
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit) // Pointers first (4 bytes each)
std::unique_ptr<socket::Socket> socket_; socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common state enum for all frame helpers // Common state enum for all frame helpers
// Note: Not all states are used by all implementations // Note: Not all states are used by all implementations

View File

@@ -24,16 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif #endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit") static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) \
do { \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -247,13 +239,12 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send 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 &name = App.get_name();
char mac[mac_len]; const std::string &mac = get_mac_address();
get_mac_address_into_buffer(mac);
// Calculate positions and sizes // Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator 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 name_offset = 1;
size_t mac_offset = name_offset + name_len; size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + mac_len; size_t total_size = 1 + name_len + mac_len;
@@ -266,7 +257,7 @@ APIError APINoiseFrameHelper::state_action_() {
// node name, terminated by null byte // node name, terminated by null byte
std::memcpy(msg.get() + name_offset, name.c_str(), name_len); std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte // node mac, terminated by null byte
std::memcpy(msg.get() + mac_offset, mac, mac_len); std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
aerr = write_frame_(msg.get(), total_size); aerr = write_frame_(msg.get(), total_size);
if (aerr != APIError::OK) if (aerr != APIError::OK)
@@ -415,7 +406,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::BAD_DATA_PACKET; return APIError::BAD_DATA_PACKET;
} }
buffer->data = msg_data + 4; // Skip 4-byte header (type + length) buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 4;
buffer->data_len = data_len; buffer->data_len = data_len;
buffer->type = type; buffer->type = type;
return APIError::OK; return APIError::OK;
@@ -442,7 +434,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());
@@ -535,7 +528,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
const auto &psk = this->ctx_.get_psk(); const auto &psk = ctx_->get_psk();
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); 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"), aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
APIError::HANDSHAKESTATE_SETUP_FAILED); APIError::HANDSHAKESTATE_SETUP_FAILED);
@@ -547,8 +540,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now // set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity) prologue_ = {};
std::vector<uint8_t>().swap(prologue_);
err = noise_handshakestate_start(handshake_); err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED); aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -9,8 +9,9 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper { class APINoiseFrameHelper final : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) { const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
// Noise header structure: // Noise header structure:
// Pos 0: indicator (0x01) // Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian) // Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -40,8 +41,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr};
// Reference to noise context (4 bytes on 32-bit) // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
APINoiseContext &ctx_; std::shared_ptr<APINoiseContext> ctx_;
// Vector (12 bytes on 32-bit) // Vector (12 bytes on 32-bit)
std::vector<uint8_t> prologue_; std::vector<uint8_t> prologue_;

View File

@@ -18,16 +18,8 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext"; static const char *const TAG = "api.plaintext";
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) \ #define HELPER_LOG(msg, ...) \
do { \ ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
char peername__[socket::PEERNAME_MAX_LEN]; \
this->socket_->getpeername_to(peername__); \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), peername__, ##__VA_ARGS__); \
} while (0)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str()) #define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
@@ -218,7 +210,8 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return aerr; return aerr;
} }
buffer->data = this->rx_buf_.data(); buffer->container = std::move(this->rx_buf_);
buffer->data_offset = 0;
buffer->data_len = this->rx_header_parsed_len_; buffer->data_len = this->rx_header_parsed_len_;
buffer->type = this->rx_header_parsed_type_; buffer->type = this->rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
@@ -237,7 +230,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
return APIError::OK; return APIError::OK;
} }
uint8_t *buffer_data = buffer.get_buffer()->data(); std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
uint8_t *buffer_data = raw_buffer->data(); // Cache buffer pointer
this->reusable_iovs_.clear(); this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size()); this->reusable_iovs_.reserve(packets.size());

View File

@@ -124,12 +124,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const {
#endif #endif
#ifdef USE_DEVICES #ifdef USE_DEVICES
for (const auto &it : this->devices) { for (const auto &it : this->devices) {
buffer.encode_message(20, it); buffer.encode_message(20, it, true);
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
for (const auto &it : this->areas) { for (const auto &it : this->areas) {
buffer.encode_message(21, it); buffer.encode_message(21, it, true);
} }
#endif #endif
#ifdef USE_AREAS #ifdef USE_AREAS
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(10, this->icon_ref_); buffer.encode_string(10, this->icon_ref_);
#endif #endif
buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
for (const char *it : *this->supported_preset_modes) { for (const auto &it : *this->supported_preset_modes) {
buffer.encode_string(12, it, strlen(it), true); buffer.encode_string(12, it, true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(13, this->device_id); buffer.encode_uint32(13, this->device_id);
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
#endif #endif
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
if (!this->supported_preset_modes->empty()) { if (!this->supported_preset_modes->empty()) {
for (const char *it : *this->supported_preset_modes) { for (const auto &it : *this->supported_preset_modes) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -447,12 +447,9 @@ bool FanCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool FanCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 13: { case 13:
// Use raw data directly to avoid allocation this->preset_mode = value.as_string();
this->preset_mode = value.data();
this->preset_mode_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -479,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
} }
buffer.encode_float(9, this->min_mireds); buffer.encode_float(9, this->min_mireds);
buffer.encode_float(10, this->max_mireds); buffer.encode_float(10, this->max_mireds);
for (const char *it : *this->effects) { for (auto &it : this->effects) {
buffer.encode_string(11, it, strlen(it), true); buffer.encode_string(11, it, true);
} }
buffer.encode_bool(13, this->disabled_by_default); buffer.encode_bool(13, this->disabled_by_default);
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
@@ -502,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const {
} }
size.add_float(1, this->min_mireds); size.add_float(1, this->min_mireds);
size.add_float(1, this->max_mireds); size.add_float(1, this->max_mireds);
if (!this->effects->empty()) { if (!this->effects.empty()) {
for (const char *it : *this->effects) { for (const auto &it : this->effects) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);
@@ -614,12 +611,9 @@ bool LightCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool LightCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 19: { case 19:
// Use raw data directly to avoid allocation this->effect = value.as_string();
this->effect = value.data();
this->effect_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -858,12 +852,9 @@ void SubscribeLogsResponse::calculate_size(ProtoSize &size) const {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool NoiseEncryptionSetKeyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: { case 1:
// Use raw data directly to avoid allocation this->key = value.as_string();
this->key = value.data();
this->key_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -884,13 +875,13 @@ void HomeassistantServiceMap::calculate_size(ProtoSize &size) const {
void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const { void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->service_ref_); buffer.encode_string(1, this->service_ref_);
for (auto &it : this->data) { for (auto &it : this->data) {
buffer.encode_message(2, it); buffer.encode_message(2, it, true);
} }
for (auto &it : this->data_template) { for (auto &it : this->data_template) {
buffer.encode_message(3, it); buffer.encode_message(3, it, true);
} }
for (auto &it : this->variables) { for (auto &it : this->variables) {
buffer.encode_message(4, it); buffer.encode_message(4, it, true);
} }
buffer.encode_bool(5, this->is_event); buffer.encode_bool(5, this->is_event);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -966,24 +957,15 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
} }
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 1: { case 1:
// Use raw data directly to avoid allocation this->entity_id = value.as_string();
this->entity_id = value.data();
this->entity_id_len = value.size();
break; break;
} case 2:
case 2: { this->state = value.as_string();
// Use raw data directly to avoid allocation
this->state = value.data();
this->state_len = value.size();
break; break;
} case 3:
case 3: { this->attribute = value.as_string();
// Use raw data directly to avoid allocation
this->attribute = value.data();
this->attribute_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -1013,7 +995,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
} }
return true; return true;
} }
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const { void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_); buffer.encode_string(1, this->name_ref_);
buffer.encode_uint32(2, static_cast<uint32_t>(this->type)); buffer.encode_uint32(2, static_cast<uint32_t>(this->type));
@@ -1026,15 +1008,13 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->name_ref_); buffer.encode_string(1, this->name_ref_);
buffer.encode_fixed32(2, this->key); buffer.encode_fixed32(2, this->key);
for (auto &it : this->args) { for (auto &it : this->args) {
buffer.encode_message(3, it); buffer.encode_message(3, it, true);
} }
buffer.encode_uint32(4, static_cast<uint32_t>(this->supports_response));
} }
void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const { void ListEntitiesServicesResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->name_ref_.size()); size.add_length(1, this->name_ref_.size());
size.add_fixed32(1, this->key); size.add_fixed32(1, this->key);
size.add_repeated_message(1, this->args); 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) { bool ExecuteServiceArgument::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {
@@ -1095,23 +1075,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
this->string_array.init(count_string_array); this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length); 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) { bool ExecuteServiceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 2: case 2:
@@ -1139,24 +1102,6 @@ void ExecuteServiceRequest::decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode(buffer, length); ProtoDecodableMessage::decode(buffer, length);
} }
#endif #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 #ifdef USE_CAMERA
void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const { void ListEntitiesCameraResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_); buffer.encode_string(1, this->object_id_ref_);
@@ -1234,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
for (const auto &it : *this->supported_swing_modes) { for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true); buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
} }
for (const char *it : *this->supported_custom_fan_modes) { for (const auto &it : *this->supported_custom_fan_modes) {
buffer.encode_string(15, it, strlen(it), true); buffer.encode_string(15, it, true);
} }
for (const auto &it : *this->supported_presets) { for (const auto &it : *this->supported_presets) {
buffer.encode_uint32(16, static_cast<uint32_t>(it), true); buffer.encode_uint32(16, static_cast<uint32_t>(it), true);
} }
for (const char *it : *this->supported_custom_presets) { for (const auto &it : *this->supported_custom_presets) {
buffer.encode_string(17, it, strlen(it), true); buffer.encode_string(17, it, true);
} }
buffer.encode_bool(18, this->disabled_by_default); buffer.encode_bool(18, this->disabled_by_default);
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
@@ -1284,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_fan_modes->empty()) { if (!this->supported_custom_fan_modes->empty()) {
for (const char *it : *this->supported_custom_fan_modes) { for (const auto &it : *this->supported_custom_fan_modes) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
if (!this->supported_presets->empty()) { if (!this->supported_presets->empty()) {
@@ -1294,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_presets->empty()) { if (!this->supported_custom_presets->empty()) {
for (const char *it : *this->supported_custom_presets) { for (const auto &it : *this->supported_custom_presets) {
size.add_length_force(2, strlen(it)); size.add_length_force(2, it.size());
} }
} }
size.add_bool(2, this->disabled_by_default); size.add_bool(2, this->disabled_by_default);
@@ -1407,18 +1352,12 @@ bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value)
} }
bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool ClimateCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 17: { case 17:
// Use raw data directly to avoid allocation this->custom_fan_mode = value.as_string();
this->custom_fan_mode = value.data();
this->custom_fan_mode_len = value.size();
break; break;
} case 21:
case 21: { this->custom_preset = value.as_string();
// Use raw data directly to avoid allocation
this->custom_preset = value.data();
this->custom_preset_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -1447,114 +1386,6 @@ bool ClimateCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
return true; return true;
} }
#endif #endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_);
buffer.encode_fixed32(2, this->key);
buffer.encode_string(3, this->name_ref_);
#ifdef USE_ENTITY_ICON
buffer.encode_string(4, this->icon_ref_);
#endif
buffer.encode_bool(5, this->disabled_by_default);
buffer.encode_uint32(6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
buffer.encode_uint32(7, this->device_id);
#endif
buffer.encode_float(8, this->min_temperature);
buffer.encode_float(9, this->max_temperature);
buffer.encode_float(10, this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
buffer.encode_uint32(11, static_cast<uint32_t>(it), true);
}
buffer.encode_uint32(12, this->supported_features);
}
void ListEntitiesWaterHeaterResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->object_id_ref_.size());
size.add_fixed32(1, this->key);
size.add_length(1, this->name_ref_.size());
#ifdef USE_ENTITY_ICON
size.add_length(1, this->icon_ref_.size());
#endif
size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_float(1, this->min_temperature);
size.add_float(1, this->max_temperature);
size.add_float(1, this->target_temperature_step);
if (!this->supported_modes->empty()) {
for (const auto &it : *this->supported_modes) {
size.add_uint32_force(1, static_cast<uint32_t>(it));
}
}
size.add_uint32(1, this->supported_features);
}
void WaterHeaterStateResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_fixed32(1, this->key);
buffer.encode_float(2, this->current_temperature);
buffer.encode_float(3, this->target_temperature);
buffer.encode_uint32(4, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
buffer.encode_uint32(5, this->device_id);
#endif
buffer.encode_uint32(6, this->state);
buffer.encode_float(7, this->target_temperature_low);
buffer.encode_float(8, this->target_temperature_high);
}
void WaterHeaterStateResponse::calculate_size(ProtoSize &size) const {
size.add_fixed32(1, this->key);
size.add_float(1, this->current_temperature);
size.add_float(1, this->target_temperature);
size.add_uint32(1, static_cast<uint32_t>(this->mode));
#ifdef USE_DEVICES
size.add_uint32(1, this->device_id);
#endif
size.add_uint32(1, this->state);
size.add_float(1, this->target_temperature_low);
size.add_float(1, this->target_temperature_high);
}
bool WaterHeaterCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2:
this->has_fields = value.as_uint32();
break;
case 3:
this->mode = static_cast<enums::WaterHeaterMode>(value.as_uint32());
break;
#ifdef USE_DEVICES
case 5:
this->device_id = value.as_uint32();
break;
#endif
case 6:
this->state = value.as_uint32();
break;
default:
return false;
}
return true;
}
bool WaterHeaterCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) {
switch (field_id) {
case 1:
this->key = value.as_fixed32();
break;
case 4:
this->target_temperature = value.as_float();
break;
case 7:
this->target_temperature_low = value.as_float();
break;
case 8:
this->target_temperature_high = value.as_float();
break;
default:
return false;
}
return true;
}
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const { void ListEntitiesNumberResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->object_id_ref_); buffer.encode_string(1, this->object_id_ref_);
@@ -1644,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
#ifdef USE_ENTITY_ICON #ifdef USE_ENTITY_ICON
buffer.encode_string(5, this->icon_ref_); buffer.encode_string(5, this->icon_ref_);
#endif #endif
for (const char *it : *this->options) { for (const auto &it : *this->options) {
buffer.encode_string(6, it, strlen(it), true); buffer.encode_string(6, it, true);
} }
buffer.encode_bool(7, this->disabled_by_default); buffer.encode_bool(7, this->disabled_by_default);
buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
@@ -1661,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
size.add_length(1, this->icon_ref_.size()); size.add_length(1, this->icon_ref_.size());
#endif #endif
if (!this->options->empty()) { if (!this->options->empty()) {
for (const char *it : *this->options) { for (const auto &it : *this->options) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);
@@ -1701,12 +1532,9 @@ bool SelectCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
} }
bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { bool SelectCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) { switch (field_id) {
case 2: { case 2:
// Use raw data directly to avoid allocation this->state = value.as_string();
this->state = value.data();
this->state_len = value.size();
break; break;
}
default: default:
return false; return false;
} }
@@ -1996,7 +1824,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_bool(8, this->supports_pause); buffer.encode_bool(8, this->supports_pause);
for (auto &it : this->supported_formats) { for (auto &it : this->supported_formats) {
buffer.encode_message(9, it); buffer.encode_message(9, it, true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id); buffer.encode_uint32(10, this->device_id);
@@ -2116,7 +1944,7 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
} }
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
for (uint16_t i = 0; i < this->advertisements_len; i++) { for (uint16_t i = 0; i < this->advertisements_len; i++) {
buffer.encode_message(1, this->advertisements[i]); buffer.encode_message(1, this->advertisements[i], true);
} }
} }
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const { void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
@@ -2189,7 +2017,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint32(2, this->handle); buffer.encode_uint32(2, this->handle);
buffer.encode_uint32(3, this->properties); buffer.encode_uint32(3, this->properties);
for (auto &it : this->descriptors) { for (auto &it : this->descriptors) {
buffer.encode_message(4, it); buffer.encode_message(4, it, true);
} }
buffer.encode_uint32(5, this->short_uuid); buffer.encode_uint32(5, this->short_uuid);
} }
@@ -2210,7 +2038,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer buffer) const {
} }
buffer.encode_uint32(2, this->handle); buffer.encode_uint32(2, this->handle);
for (auto &it : this->characteristics) { for (auto &it : this->characteristics) {
buffer.encode_message(3, it); buffer.encode_message(3, it, true);
} }
buffer.encode_uint32(4, this->short_uuid); buffer.encode_uint32(4, this->short_uuid);
} }
@@ -2226,7 +2054,7 @@ void BluetoothGATTService::calculate_size(ProtoSize &size) const {
void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_uint64(1, this->address); buffer.encode_uint64(1, this->address);
for (auto &it : this->services) { for (auto &it : this->services) {
buffer.encode_message(2, it); buffer.encode_message(2, it, true);
} }
} }
void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const { void BluetoothGATTGetServicesResponse::calculate_size(ProtoSize &size) const {
@@ -2686,7 +2514,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL
} }
void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const { void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer buffer) const {
for (auto &it : this->available_wake_words) { for (auto &it : this->available_wake_words) {
buffer.encode_message(1, it); buffer.encode_message(1, it, true);
} }
for (const auto &it : *this->active_wake_words) { for (const auto &it : *this->active_wake_words) {
buffer.encode_string(2, it, true); buffer.encode_string(2, it, true);
@@ -3049,8 +2877,8 @@ void ListEntitiesEventResponse::encode(ProtoWriteBuffer buffer) const {
buffer.encode_bool(6, this->disabled_by_default); buffer.encode_bool(6, this->disabled_by_default);
buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category)); buffer.encode_uint32(7, static_cast<uint32_t>(this->entity_category));
buffer.encode_string(8, this->device_class_ref_); buffer.encode_string(8, this->device_class_ref_);
for (const char *it : *this->event_types) { for (auto &it : this->event_types) {
buffer.encode_string(9, it, strlen(it), true); buffer.encode_string(9, it, true);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES
buffer.encode_uint32(10, this->device_id); buffer.encode_uint32(10, this->device_id);
@@ -3066,9 +2894,9 @@ void ListEntitiesEventResponse::calculate_size(ProtoSize &size) const {
size.add_bool(1, this->disabled_by_default); size.add_bool(1, this->disabled_by_default);
size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
size.add_length(1, this->device_class_ref_.size()); size.add_length(1, this->device_class_ref_.size());
if (!this->event_types->empty()) { if (!this->event_types.empty()) {
for (const char *it : *this->event_types) { for (const auto &it : this->event_types) {
size.add_length_force(1, strlen(it)); size.add_length_force(1, it.size());
} }
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

View File

@@ -51,7 +51,6 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_MEASUREMENT = 1, STATE_CLASS_MEASUREMENT = 1,
STATE_CLASS_TOTAL_INCREASING = 2, STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3, STATE_CLASS_TOTAL = 3,
STATE_CLASS_MEASUREMENT_ANGLE = 4,
}; };
#endif #endif
enum LogLevel : uint32_t { enum LogLevel : uint32_t {
@@ -64,7 +63,7 @@ enum LogLevel : uint32_t {
LOG_LEVEL_VERBOSE = 6, LOG_LEVEL_VERBOSE = 6,
LOG_LEVEL_VERY_VERBOSE = 7, LOG_LEVEL_VERY_VERBOSE = 7,
}; };
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
enum ServiceArgType : uint32_t { enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_BOOL = 0, SERVICE_ARG_TYPE_BOOL = 0,
SERVICE_ARG_TYPE_INT = 1, SERVICE_ARG_TYPE_INT = 1,
@@ -75,12 +74,6 @@ enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6, SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
SERVICE_ARG_TYPE_STRING_ARRAY = 7, 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 #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
enum ClimateMode : uint32_t { enum ClimateMode : uint32_t {
@@ -129,25 +122,6 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7, CLIMATE_PRESET_ACTIVITY = 7,
}; };
#endif #endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER #ifdef USE_NUMBER
enum NumberMode : uint32_t { enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0, NUMBER_MODE_AUTO = 0,
@@ -751,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
const std::vector<const char *> *supported_preset_modes{}; const std::set<std::string> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -784,7 +758,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
class FanCommandRequest final : public CommandProtoMessage { class FanCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 31; static constexpr uint8_t MESSAGE_TYPE = 31;
static constexpr uint8_t ESTIMATED_SIZE = 48; static constexpr uint8_t ESTIMATED_SIZE = 38;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "fan_command_request"; } const char *message_name() const override { return "fan_command_request"; }
#endif #endif
@@ -797,8 +771,7 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false}; bool has_speed_level{false};
int32_t speed_level{0}; int32_t speed_level{0};
bool has_preset_mode{false}; bool has_preset_mode{false};
const uint8_t *preset_mode{nullptr}; std::string preset_mode{};
uint16_t preset_mode_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -820,7 +793,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
const light::ColorModeMask *supported_color_modes{}; const light::ColorModeMask *supported_color_modes{};
float min_mireds{0.0f}; float min_mireds{0.0f};
float max_mireds{0.0f}; float max_mireds{0.0f};
const FixedVector<const char *> *effects{}; std::vector<std::string> effects{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -860,7 +833,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
class LightCommandRequest final : public CommandProtoMessage { class LightCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 32; static constexpr uint8_t MESSAGE_TYPE = 32;
static constexpr uint8_t ESTIMATED_SIZE = 122; static constexpr uint8_t ESTIMATED_SIZE = 112;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "light_command_request"; } const char *message_name() const override { return "light_command_request"; }
#endif #endif
@@ -889,8 +862,7 @@ class LightCommandRequest final : public CommandProtoMessage {
bool has_flash_length{false}; bool has_flash_length{false};
uint32_t flash_length{0}; uint32_t flash_length{0};
bool has_effect{false}; bool has_effect{false};
const uint8_t *effect{nullptr}; std::string effect{};
uint16_t effect_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1073,12 +1045,11 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage { class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 124; static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 19; static constexpr uint8_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; } const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif #endif
const uint8_t *key{nullptr}; std::string key{};
uint16_t key_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1222,16 +1193,13 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
class HomeAssistantStateResponse final : public ProtoDecodableMessage { class HomeAssistantStateResponse final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 40; static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 57; static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; } const char *message_name() const override { return "home_assistant_state_response"; }
#endif #endif
const uint8_t *entity_id{nullptr}; std::string entity_id{};
uint16_t entity_id_len{0}; std::string state{};
const uint8_t *state{nullptr}; std::string attribute{};
uint16_t state_len{0};
const uint8_t *attribute{nullptr};
uint16_t attribute_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -1271,7 +1239,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
class ListEntitiesServicesArgument final : public ProtoMessage { class ListEntitiesServicesArgument final : public ProtoMessage {
public: public:
StringRef name_ref_{}; StringRef name_ref_{};
@@ -1288,7 +1256,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage { class ListEntitiesServicesResponse final : public ProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 41; static constexpr uint8_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 50; static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_services_response"; } const char *message_name() const override { return "list_entities_services_response"; }
#endif #endif
@@ -1296,7 +1264,6 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
void set_name(const StringRef &ref) { this->name_ref_ = ref; } void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0}; uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{}; FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1329,18 +1296,12 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
class ExecuteServiceRequest final : public ProtoDecodableMessage { class ExecuteServiceRequest final : public ProtoDecodableMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 42; static constexpr uint8_t MESSAGE_TYPE = 42;
static constexpr uint8_t ESTIMATED_SIZE = 45; static constexpr uint8_t ESTIMATED_SIZE = 39;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "execute_service_request"; } const char *message_name() const override { return "execute_service_request"; }
#endif #endif
uint32_t key{0}; uint32_t key{0};
FixedVector<ExecuteServiceArgument> args{}; 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; void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
@@ -1349,32 +1310,6 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited 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 #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
@@ -1442,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
#endif #endif
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
const climate::ClimateModeMask *supported_modes{}; const std::set<climate::ClimateMode> *supported_modes{};
float visual_min_temperature{0.0f}; float visual_min_temperature{0.0f};
float visual_max_temperature{0.0f}; float visual_max_temperature{0.0f};
float visual_target_temperature_step{0.0f}; float visual_target_temperature_step{0.0f};
bool supports_action{false}; bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{}; const std::set<climate::ClimateFanMode> *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{}; const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
const std::vector<const char *> *supported_custom_fan_modes{}; const std::set<std::string> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{}; const std::set<climate::ClimatePreset> *supported_presets{};
const std::vector<const char *> *supported_custom_presets{}; const std::set<std::string> *supported_custom_presets{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@@ -1499,7 +1434,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
class ClimateCommandRequest final : public CommandProtoMessage { class ClimateCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 48; static constexpr uint8_t MESSAGE_TYPE = 48;
static constexpr uint8_t ESTIMATED_SIZE = 104; static constexpr uint8_t ESTIMATED_SIZE = 84;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "climate_command_request"; } const char *message_name() const override { return "climate_command_request"; }
#endif #endif
@@ -1516,13 +1451,11 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false}; bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{}; enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false}; bool has_custom_fan_mode{false};
const uint8_t *custom_fan_mode{nullptr}; std::string custom_fan_mode{};
uint16_t custom_fan_mode_len{0};
bool has_preset{false}; bool has_preset{false};
enums::ClimatePreset preset{}; enums::ClimatePreset preset{};
bool has_custom_preset{false}; bool has_custom_preset{false};
const uint8_t *custom_preset{nullptr}; std::string custom_preset{};
uint16_t custom_preset_len{0};
bool has_target_humidity{false}; bool has_target_humidity{false};
float target_humidity{0.0f}; float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1535,70 +1468,6 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
#endif #endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
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:
};
class WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
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:
};
class WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage { class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public: public:
@@ -1665,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_select_response"; } const char *message_name() const override { return "list_entities_select_response"; }
#endif #endif
const FixedVector<const char *> *options{}; const std::vector<std::string> *options{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1695,12 +1564,11 @@ class SelectStateResponse final : public StateResponseProtoMessage {
class SelectCommandRequest final : public CommandProtoMessage { class SelectCommandRequest final : public CommandProtoMessage {
public: public:
static constexpr uint8_t MESSAGE_TYPE = 54; static constexpr uint8_t MESSAGE_TYPE = 54;
static constexpr uint8_t ESTIMATED_SIZE = 28; static constexpr uint8_t ESTIMATED_SIZE = 18;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_command_request"; } const char *message_name() const override { return "select_command_request"; }
#endif #endif
const uint8_t *state{nullptr}; std::string state{};
uint16_t state_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override; void dump_to(std::string &out) const override;
#endif #endif
@@ -2920,7 +2788,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#endif #endif
StringRef device_class_ref_{}; StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; } void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
const FixedVector<const char *> *event_types{}; std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override; void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -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) { static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) {
char buffer[64]; char buffer[64];
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);
snprintf(buffer, 64, "%" PRIu64, value); snprintf(buffer, 64, "%llu", value);
append_with_newline(out, buffer); append_with_newline(out, buffer);
} }
@@ -88,12 +88,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
out.append("\n"); out.append("\n");
} }
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append(value).append("'");
out.append("\n");
}
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) { template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
append_field_prefix(out, field_name, indent); append_field_prefix(out, field_name, indent);
out.append(proto_enum_to_string<T>(value)); out.append(proto_enum_to_string<T>(value));
@@ -179,8 +173,6 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
return "STATE_CLASS_TOTAL_INCREASING"; return "STATE_CLASS_TOTAL_INCREASING";
case enums::STATE_CLASS_TOTAL: case enums::STATE_CLASS_TOTAL:
return "STATE_CLASS_TOTAL"; return "STATE_CLASS_TOTAL";
case enums::STATE_CLASS_MEASUREMENT_ANGLE:
return "STATE_CLASS_MEASUREMENT_ANGLE";
default: default:
return "UNKNOWN"; return "UNKNOWN";
} }
@@ -208,7 +200,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
return "UNKNOWN"; return "UNKNOWN";
} }
} }
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) { template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
switch (value) { switch (value) {
case enums::SERVICE_ARG_TYPE_BOOL: case enums::SERVICE_ARG_TYPE_BOOL:
@@ -231,20 +223,6 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
return "UNKNOWN"; 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 #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) { template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
@@ -348,47 +326,6 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
} }
} }
#endif #endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER #ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) { template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) { switch (value) {
@@ -964,9 +901,7 @@ void FanCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_speed_level", this->has_speed_level); dump_field(out, "has_speed_level", this->has_speed_level);
dump_field(out, "speed_level", this->speed_level); dump_field(out, "speed_level", this->speed_level);
dump_field(out, "has_preset_mode", this->has_preset_mode); dump_field(out, "has_preset_mode", this->has_preset_mode);
out.append(" preset_mode: "); dump_field(out, "preset_mode", this->preset_mode);
out.append(format_hex_pretty(this->preset_mode, this->preset_mode_len));
out.append("\n");
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif
@@ -983,7 +918,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
} }
dump_field(out, "min_mireds", this->min_mireds); dump_field(out, "min_mireds", this->min_mireds);
dump_field(out, "max_mireds", this->max_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, "effects", it, 4);
} }
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
@@ -1042,9 +977,7 @@ void LightCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_flash_length", this->has_flash_length); dump_field(out, "has_flash_length", this->has_flash_length);
dump_field(out, "flash_length", this->flash_length); dump_field(out, "flash_length", this->flash_length);
dump_field(out, "has_effect", this->has_effect); dump_field(out, "has_effect", this->has_effect);
out.append(" effect: "); dump_field(out, "effect", this->effect);
out.append(format_hex_pretty(this->effect, this->effect_len));
out.append("\n");
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif
@@ -1156,7 +1089,7 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: "); out.append(" key: ");
out.append(format_hex_pretty(this->key, this->key_len)); out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append("\n"); out.append("\n");
} }
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); } void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
@@ -1225,15 +1158,9 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
} }
void HomeAssistantStateResponse::dump_to(std::string &out) const { void HomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeAssistantStateResponse"); MessageDumpHelper helper(out, "HomeAssistantStateResponse");
out.append(" entity_id: "); dump_field(out, "entity_id", this->entity_id);
out.append(format_hex_pretty(this->entity_id, this->entity_id_len)); dump_field(out, "state", this->state);
out.append("\n"); dump_field(out, "attribute", this->attribute);
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
out.append(" attribute: ");
out.append(format_hex_pretty(this->attribute, this->attribute_len));
out.append("\n");
} }
#endif #endif
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }
@@ -1244,7 +1171,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
out.append(format_hex_pretty(this->timezone, this->timezone_len)); out.append(format_hex_pretty(this->timezone, this->timezone_len));
out.append("\n"); out.append("\n");
} }
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void ListEntitiesServicesArgument::dump_to(std::string &out) const { void ListEntitiesServicesArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesServicesArgument"); MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
dump_field(out, "name", this->name_ref_); dump_field(out, "name", this->name_ref_);
@@ -1259,7 +1186,6 @@ void ListEntitiesServicesResponse::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); out.append("\n");
} }
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
} }
void ExecuteServiceArgument::dump_to(std::string &out) const { void ExecuteServiceArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ExecuteServiceArgument"); MessageDumpHelper helper(out, "ExecuteServiceArgument");
@@ -1289,25 +1215,6 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
it.dump_to(out); it.dump_to(out);
out.append("\n"); 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 #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
@@ -1423,15 +1330,11 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
dump_field(out, "has_swing_mode", this->has_swing_mode); dump_field(out, "has_swing_mode", this->has_swing_mode);
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode)); dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode); dump_field(out, "has_custom_fan_mode", this->has_custom_fan_mode);
out.append(" custom_fan_mode: "); dump_field(out, "custom_fan_mode", this->custom_fan_mode);
out.append(format_hex_pretty(this->custom_fan_mode, this->custom_fan_mode_len));
out.append("\n");
dump_field(out, "has_preset", this->has_preset); dump_field(out, "has_preset", this->has_preset);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset)); dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "has_custom_preset", this->has_custom_preset); dump_field(out, "has_custom_preset", this->has_custom_preset);
out.append(" custom_preset: "); dump_field(out, "custom_preset", this->custom_preset);
out.append(format_hex_pretty(this->custom_preset, this->custom_preset_len));
out.append("\n");
dump_field(out, "has_target_humidity", this->has_target_humidity); dump_field(out, "has_target_humidity", this->has_target_humidity);
dump_field(out, "target_humidity", this->target_humidity); dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES #ifdef USE_DEVICES
@@ -1439,55 +1342,6 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif #endif
} }
#endif #endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const { void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse"); MessageDumpHelper helper(out, "ListEntitiesNumberResponse");
@@ -1557,9 +1411,7 @@ void SelectStateResponse::dump_to(std::string &out) const {
void SelectCommandRequest::dump_to(std::string &out) const { void SelectCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "SelectCommandRequest"); MessageDumpHelper helper(out, "SelectCommandRequest");
dump_field(out, "key", this->key); dump_field(out, "key", this->key);
out.append(" state: "); dump_field(out, "state", this->state);
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
#ifdef USE_DEVICES #ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id); dump_field(out, "device_id", this->device_id);
#endif #endif
@@ -2195,7 +2047,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
dump_field(out, "disabled_by_default", this->disabled_by_default); dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category)); dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class_ref_); dump_field(out, "device_class", this->device_class_ref_);
for (const auto &it : *this->event_types) { for (const auto &it : this->event_types) {
dump_field(out, "event_types", it, 4); dump_field(out, "event_types", it, 4);
} }
#ifdef USE_DEVICES #ifdef USE_DEVICES

View File

@@ -10,10 +10,6 @@
#include "esphome/components/climate/climate_traits.h" #include "esphome/components/climate/climate_traits.h"
#endif #endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h" #include "esphome/components/light/light_traits.h"
#endif #endif

View File

@@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
} }
#endif #endif
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
switch (msg_type) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: { case HelloRequest::MESSAGE_TYPE: {
HelloRequest msg; HelloRequest msg;
@@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
break; break;
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
case ExecuteServiceRequest::MESSAGE_TYPE: { case ExecuteServiceRequest::MESSAGE_TYPE: {
ExecuteServiceRequest msg; ExecuteServiceRequest msg;
msg.decode(msg_data, msg_size); msg.decode(msg_data, msg_size);
@@ -621,17 +621,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg); this->on_homeassistant_action_response(msg);
break; break;
} }
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif #endif
default: default:
break; break;
@@ -681,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
this->subscribe_home_assistant_states(msg); this->subscribe_home_assistant_states(msg);
} }
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); } void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -838,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); } void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
#endif #endif
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
// Check authentication/connection requirements for messages // Check authentication/connection requirements for messages
switch (msg_type) { switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required case HelloRequest::MESSAGE_TYPE: // No setup required

View File

@@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_get_time_response(const GetTimeResponse &value){}; virtual void on_get_time_response(const GetTimeResponse &value){};
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
virtual void on_execute_service_request(const ExecuteServiceRequest &value){}; virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
#endif #endif
@@ -91,10 +91,6 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){}; virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif #endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){}; virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif #endif
@@ -222,7 +218,7 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif #endif
protected: protected:
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
}; };
class APIServerConnection : public APIServerConnectionBase { class APIServerConnection : public APIServerConnectionBase {
@@ -243,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
virtual void execute_service(const ExecuteServiceRequest &msg) = 0; virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -372,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void on_execute_service_request(const ExecuteServiceRequest &msg) override; void on_execute_service_request(const ExecuteServiceRequest &msg) override;
#endif #endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
@@ -484,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_ZWAVE_PROXY #ifdef USE_ZWAVE_PROXY
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
#endif #endif
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -4,7 +4,6 @@
#include "api_connection.h" #include "api_connection.h"
#include "esphome/components/network/util.h" #include "esphome/components/network/util.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -35,7 +34,7 @@ APIServer::APIServer() {
} }
void APIServer::setup() { void APIServer::setup() {
ControllerRegistry::register_controller(this); this->setup_controller();
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
uint32_t hash = 88491486UL; uint32_t hash = 88491486UL;
@@ -52,6 +51,11 @@ void APIServer::setup() {
#endif #endif
#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 this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) { if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket"); ESP_LOGW(TAG, "Could not create socket");
@@ -96,22 +100,42 @@ void APIServer::setup() {
#ifdef USE_LOGGER #ifdef USE_LOGGER
if (logger::global_logger != nullptr) { if (logger::global_logger != nullptr) {
logger::global_logger->add_log_listener(this); 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);
}
});
} }
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) { if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_listener(this); 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);
}
});
} }
#endif #endif
}
// Initialize last_connected_ for reboot timeout tracking void APIServer::schedule_reboot_timeout_() {
this->last_connected_ = App.get_loop_component_start_time(); this->status_set_warning();
// Set warning status if reboot timeout is enabled this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (this->reboot_timeout_ != 0) { if (!global_api_server->is_connected()) {
this->status_set_warning(); ESP_LOGE(TAG, "No clients; rebooting");
} App.reboot();
}
});
} }
void APIServer::loop() { void APIServer::loop() {
@@ -125,41 +149,29 @@ void APIServer::loop() {
if (!sock) if (!sock)
break; break;
char peername[socket::PEERNAME_MAX_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit // Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) { if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername); ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
// Immediately close - socket destructor will handle cleanup // Immediately close - socket destructor will handle cleanup
sock.reset(); sock.reset();
continue; continue;
} }
ESP_LOGD(TAG, "Accept %s", peername); ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this); auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn); this->clients_.emplace_back(conn);
conn->start(); conn->start();
// First client connected - clear warning and update timestamp // Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) { if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning(); this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time(); this->cancel_timeout("api_reboot");
} }
} }
} }
if (this->clients_.empty()) { 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; return;
} }
@@ -169,7 +181,8 @@ void APIServer::loop() {
// Network is down - disconnect all clients // Network is down - disconnect all clients
for (auto &client : this->clients_) { for (auto &client : this->clients_) {
client->on_fatal_error(); client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect")); ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
} }
// Continue to process and clean up the clients below // Continue to process and clean up the clients below
} }
@@ -187,11 +200,7 @@ void APIServer::loop() {
// Rare case: handle disconnection // Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Trigger expects std::string, get fresh peername from socket this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->get_peername());
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif #endif
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str()); ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
@@ -201,10 +210,9 @@ void APIServer::loop() {
} }
this->clients_.pop_back(); this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout // Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) { if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning(); this->schedule_reboot_timeout_();
this->last_connected_ = App.get_loop_component_start_time();
} }
// Don't increment client_index since we need to process the swapped element // Don't increment client_index since we need to process the swapped element
} }
@@ -216,10 +224,10 @@ void APIServer::dump_config() {
" Address: %s:%u\n" " Address: %s:%u\n"
" Listen backlog: %u\n" " Listen backlog: %u\n"
" Max connections: %u", " Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_); network::get_use_address().c_str(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk())); ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
if (!this->noise_ctx_.has_psk()) { if (!this->noise_ctx_->has_psk()) {
ESP_LOGCONFIG(TAG, " Supports encryption: YES"); ESP_LOGCONFIG(TAG, " Supports encryption: YES");
} }
#else #else
@@ -261,7 +269,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
void APIServer::handle_disconnect(APIConnection *conn) {} void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for controller update dispatch // Macro for entities without extra parameters
#define API_DISPATCH_UPDATE(entity_type, entity_name) \ #define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \ void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \ if (obj->is_internal()) \
@@ -270,6 +278,15 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
c->send_##entity_name##_state(obj); \ c->send_##entity_name##_state(obj); \
} }
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor) API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif #endif
@@ -287,15 +304,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
API_DISPATCH_UPDATE(sensor::Sensor, sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
API_DISPATCH_UPDATE(switch_::Switch, switch) API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor) API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
@@ -303,7 +320,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
API_DISPATCH_UPDATE(number::Number, number) API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -319,11 +336,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
API_DISPATCH_UPDATE(text::Text, text) API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
API_DISPATCH_UPDATE(select::Select, select) API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
@@ -338,18 +355,13 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player) API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif #endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields, // Event is a special case - it's the only entity that passes extra parameters to the send method
// events store their state in a member accessed via obj->get_last_event_type() void APIServer::on_event(event::Event *obj, const std::string &event_type) {
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal()) if (obj->is_internal())
return; return;
for (auto &c : this->clients_) for (auto &c : this->clients_)
c->send_event(obj, obj->get_last_event_type()); c->send_event(obj, event_type);
} }
#endif #endif
@@ -426,56 +438,25 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication) void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f) {
std::function<void(const std::string &)> f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{ this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once, .entity_id = std::move(entity_id),
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation) .attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
}); });
} }
// 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(const 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(const 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(const 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(const std::string &)> f) {
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, void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) { std::function<void(std::string)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true); this->state_subs_.push_back(HomeAssistantStateSubscription{
} .entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const { const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_; return this->state_subs_;
@@ -487,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; }
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool APIServer::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) {
if (!this->noise_pref_.save(&new_psk)) {
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
return false;
}
// ensure it's written immediately
if (!global_preferences->sync()) {
ESP_LOGW(TAG, "Failed to sync preferences");
return false;
}
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
if (make_active) {
this->set_timeout(100, [this, active_psk]() {
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
this->set_noise_psk(active_psk);
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
}
bool APIServer::save_noise_psk(psk_t psk, bool make_active) { bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
#ifdef USE_API_NOISE_PSK_FROM_YAML #ifdef USE_API_NOISE_PSK_FROM_YAML
// When PSK is set from YAML, this function should never be called // When PSK is set from YAML, this function should never be called
@@ -519,28 +475,34 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
ESP_LOGW(TAG, "Key set in YAML"); ESP_LOGW(TAG, "Key set in YAML");
return false; return false;
#else #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())) { if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
ESP_LOGW(TAG, "New PSK matches old"); ESP_LOGW(TAG, "New PSK matches old");
return true; return true;
} }
SavedNoisePsk new_saved_psk{psk}; SavedNoisePsk new_saved_psk{psk};
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, if (!this->noise_pref_.save(&new_saved_psk)) {
make_active); ESP_LOGW(TAG, "Failed to save Noise PSK");
#endif return false;
} }
bool APIServer::clear_noise_psk(bool make_active) { // ensure it's written immediately
#ifdef USE_API_NOISE_PSK_FROM_YAML if (!global_preferences->sync()) {
// When PSK is set from YAML, this function should never be called ESP_LOGW(TAG, "Failed to sync preferences");
// but if it is, reject the change return false;
ESP_LOGW(TAG, "Key set in YAML"); }
return false; ESP_LOGD(TAG, "Noise PSK saved");
#else if (make_active) {
SavedNoisePsk empty_psk{}; this->set_timeout(100, [this, psk]() {
psk_t empty{}; ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, this->set_noise_psk(psk);
make_active); for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
}
});
}
return true;
#endif #endif
} }
#endif #endif
@@ -554,42 +516,7 @@ void APIServer::request_time() {
} }
#endif #endif
bool APIServer::is_connected(bool state_subscription_only) const { bool APIServer::is_connected() const { return !this->clients_.empty(); }
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() { void APIServer::on_shutdown() {
this->shutting_down_ = true; this->shutting_down_ = true;
@@ -626,84 +553,5 @@ bool APIServer::teardown() {
return this->clients_.empty(); 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 } // namespace esphome::api
#endif #endif

View File

@@ -12,39 +12,22 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "list_entities.h" #include "list_entities.h"
#include "subscribe_state.h" #include "subscribe_state.h"
#ifdef USE_LOGGER #ifdef USE_API_SERVICES
#include "esphome/components/logger/logger.h" #include "user_services.h"
#endif
#ifdef USE_CAMERA
#include "esphome/components/camera/camera.h"
#endif #endif
#include <map>
#include <vector> #include <vector>
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS
// Forward declaration - full definition in user_services.h
class UserServiceDescriptor;
#endif
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
struct SavedNoisePsk { struct SavedNoisePsk {
psk_t psk; psk_t psk;
} PACKED; // NOLINT } PACKED; // NOLINT
#endif #endif
class APIServer : public Component, class APIServer : public Component, public Controller {
public Controller
#ifdef USE_LOGGER
,
public logger::LogListener
#endif
#ifdef USE_CAMERA
,
public camera::CameraListener
#endif
{
public: public:
APIServer(); APIServer();
void setup() override; void setup() override;
@@ -54,12 +37,6 @@ class APIServer : public Component,
void dump_config() override; void dump_config() override;
void on_shutdown() override; void on_shutdown() override;
bool teardown() 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 #ifdef USE_API_PASSWORD
bool check_password(const uint8_t *password_data, size_t password_len) const; bool check_password(const uint8_t *password_data, size_t password_len) const;
void set_password(const std::string &password); void set_password(const std::string &password);
@@ -76,9 +53,8 @@ class APIServer : public Component,
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
bool save_noise_psk(psk_t psk, bool make_active = true); 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); }
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); } std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
#endif // USE_API_NOISE #endif // USE_API_NOISE
void handle_disconnect(APIConnection *conn); void handle_disconnect(APIConnection *conn);
@@ -95,19 +71,19 @@ class APIServer : public Component,
void on_light_update(light::LightState *obj) override; void on_light_update(light::LightState *obj) override;
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj) override; void on_sensor_update(sensor::Sensor *obj, float state) override;
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj) override; void on_switch_update(switch_::Switch *obj, bool state) override;
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj) override; void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override; void on_climate_update(climate::Climate *obj) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
void on_number_update(number::Number *obj) override; void on_number_update(number::Number *obj, float state) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override; void on_date_update(datetime::DateEntity *obj) override;
@@ -119,10 +95,10 @@ class APIServer : public Component,
void on_datetime_update(datetime::DateTimeEntity *obj) override; void on_datetime_update(datetime::DateTimeEntity *obj) override;
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
void on_text_update(text::Text *obj) override; void on_text_update(text::Text *obj, const std::string &state) override;
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
void on_select_update(select::Select *obj) override; void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override; void on_lock_update(lock::Lock *obj) override;
@@ -133,9 +109,6 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override; void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif #endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES #ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call); void send_homeassistant_action(const HomeassistantActionRequest &call);
@@ -150,28 +123,9 @@ class APIServer : public Component,
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES #endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
this->user_services_.assign(services);
}
#ifdef USE_API_CUSTOM_SERVICES
// Only compile push_back method when custom_services: true (external components)
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif #endif
#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 #ifdef USE_HOMEASSISTANT_TIME
void request_time(); void request_time();
#endif #endif
@@ -180,7 +134,7 @@ class APIServer : public Component,
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void on_event(event::Event *obj) override; void on_event(event::Event *obj, const std::string &event_type) override;
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override; void on_update(update::UpdateEntity *obj) override;
@@ -189,36 +143,23 @@ class APIServer : public Component,
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
#endif #endif
bool is_connected(bool state_subscription_only = false) const; bool is_connected() const;
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription { struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external) std::string entity_id;
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute) optional<std::string> attribute;
std::function<void(const std::string &)> callback; std::function<void(std::string)> callback;
bool once; 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(const std::string &)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(const 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, void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f); std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute, void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f); std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const; const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; } const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
#endif #endif
@@ -232,17 +173,7 @@ class APIServer : public Component,
#endif #endif
protected: protected:
#ifdef USE_API_NOISE void schedule_reboot_timeout_();
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(const std::string &)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each) // Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr; std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER #ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -254,7 +185,6 @@ class APIServer : public Component,
// 4-byte aligned types // 4-byte aligned types
uint32_t reboot_timeout_{300000}; uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit) // Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_; std::vector<std::unique_ptr<APIConnection>> clients_;
@@ -265,19 +195,8 @@ class APIServer : public Component,
#ifdef USE_API_HOMEASSISTANT_STATES #ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_; std::vector<HomeAssistantStateSubscription> state_subs_;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
std::vector<UserServiceDescriptor *> user_services_; 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 #endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse { struct PendingActionResponse {
@@ -298,7 +217,7 @@ class APIServer : public Component,
// 7 bytes used, 1 byte padding // 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
APINoiseContext noise_ctx_; std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
ESPPreferenceObject noise_pref_; ESPPreferenceObject noise_pref_;
#endif // USE_API_NOISE #endif // USE_API_NOISE
}; };
@@ -306,11 +225,8 @@ class APIServer : public Component,
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
TEMPLATABLE_VALUE(bool, state_subscription_only)
public: public:
bool check(const Ts &...x) override { bool check(Ts... x) override { return global_api_server->is_connected(); }
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
}
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -3,28 +3,25 @@
#include <map> #include <map>
#include "api_server.h" #include "api_server.h"
#ifdef USE_API #ifdef USE_API
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
#include "user_services.h" #include "user_services.h"
#endif #endif
namespace esphome::api { namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> { template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceBase<Ts...> {
public: public:
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj, CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
void (T::*callback)(Ts...)) void (T::*callback)(Ts...))
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {} : UserServiceBase<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected: protected:
// CustomAPIDevice services don't support action responses - ignore call_id and return_response void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
(this->obj_->*this->callback_)(x...); // NOLINT
}
T *obj_; T *obj_;
void (T::*callback_)(Ts...); void (T::*callback_)(Ts...);
}; };
#endif // USE_API_USER_DEFINED_ACTIONS #endif // USE_API_SERVICES
class CustomAPIDevice { class CustomAPIDevice {
public: public:
@@ -52,18 +49,12 @@ class CustomAPIDevice {
* @param name The name of the service to register. * @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. * @param arg_names The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T, typename... Ts> template<typename T, typename... Ts>
void register_service(void (T::*callback)(Ts...), const std::string &name, void register_service(void (T::*callback)(Ts...), const std::string &name,
const std::array<std::string, sizeof...(Ts)> &arg_names) { const std::array<std::string, sizeof...(Ts)> &arg_names) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T, typename... Ts> template<typename T, typename... Ts>
@@ -93,16 +84,10 @@ class CustomAPIDevice {
* @param callback The member function to call when the service is triggered. * @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. * @param name The name of the arguments for the service, must match the arguments of the function.
*/ */
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
#ifdef USE_API_CUSTOM_SERVICES
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
global_api_server->register_user_service(service); global_api_server->register_user_service(service);
#else
static_assert(
sizeof(T) == 0,
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
#endif
} }
#else #else
template<typename T> void register_service(void (T::*callback)(), const std::string &name) { template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
@@ -122,7 +107,7 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
* } * }
* *
* void on_state_changed(const std::string &state) { * void on_state_changed(std::string state) {
* // State of sensor.weather_forecast is `state` * // State of sensor.weather_forecast is `state`
* } * }
* ``` * ```
@@ -133,7 +118,7 @@ class CustomAPIDevice {
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1); auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
@@ -148,7 +133,7 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast"); * subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
* } * }
* *
* void on_state_changed(const std::string &entity_id, const std::string &state) { * void on_state_changed(std::string entity_id, std::string state) {
* // State of `entity_id` is `state` * // State of `entity_id` is `state`
* } * }
* ``` * ```
@@ -159,14 +144,14 @@ class CustomAPIDevice {
* @param attribute The entity state attribute to track. * @param attribute The entity state attribute to track.
*/ */
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, const std::string &), void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &entity_id, const std::string &attribute = "") { const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1); auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f); global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
} }
#else #else
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &), const std::string &entity_id, void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") { const std::string &attribute = "") {
static_assert(sizeof(T) == 0, static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
@@ -174,8 +159,8 @@ class CustomAPIDevice {
} }
template<typename T> template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, const std::string &), void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &entity_id, const std::string &attribute = "") { const std::string &attribute = "") {
static_assert(sizeof(T) == 0, static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section " "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration"); "of your YAML configuration");

View File

@@ -12,17 +12,10 @@
#endif #endif
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome::api { namespace esphome::api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> { 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: private:
// Helper to convert value to string - handles the case where value is already a string // 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)); } template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
@@ -53,25 +46,23 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event") // 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. // and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using const char* avoids std::string heap allocation - keys remain in flash. // Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {} template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
const char *key{nullptr}; std::string key;
TemplatableStringValue<Ts...> value; TemplatableStringValue<Ts...> value;
}; };
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action // 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 { class ActionResponse {
public: public:
ActionResponse(bool success, const std::string &error_message) : success_(success), error_message_(error_message) {} ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, const std::string &error_message, const uint8_t *data, size_t data_len) ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(error_message) { : success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0) if (data == nullptr || data_len == 0)
return; return;
this->json_document_ = json::parse_json(data, data_len); this->json_document_ = json::parse_json(data, data_len);
@@ -79,8 +70,7 @@ class ActionResponse {
#endif #endif
bool is_success() const { return this->success_; } bool is_success() const { return this->success_; }
// Returns reference to error message - can be implicitly converted to std::string if needed const std::string &get_error_message() const { return this->error_message_; }
const StringRef &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view) // Get data as parsed JSON object (const version returns read-only view)
@@ -89,7 +79,7 @@ class ActionResponse {
protected: protected:
bool success_; bool success_;
StringRef error_message_; std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_; JsonDocument json_document_;
#endif #endif
@@ -115,15 +105,14 @@ 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))). // 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. // The value parameter can be a lambda/template, but keys are never templatable.
// Using const char* for keys avoids std::string heap allocation - keys remain in flash. template<typename K, typename V> void add_data(K &&key, V &&value) {
template<typename V> void add_data(const char *key, V &&value) { this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
this->add_kv_(this->data_, key, std::forward<V>(value));
} }
template<typename V> void add_data_template(const char *key, V &&value) { template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, key, std::forward<V>(value)); this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
} }
template<typename V> void add_variable(const char *key, V &&value) { template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, key, std::forward<V>(value)); this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
} }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -144,7 +133,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; } Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
void play(const Ts &...x) override { void play(Ts... x) override {
HomeassistantActionRequest resp; HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...); std::string service_value = this->service_.value(x...);
resp.set_service(StringRef(service_value)); resp.set_service(StringRef(service_value));
@@ -196,11 +185,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
} }
protected: protected:
// Helper to add key-value pairs to FixedVectors // Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
// Keys are always string literals (const char*), values can be lambdas/templates template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
auto &kv = vec.emplace_back(); auto &kv = vec.emplace_back();
kv.key = key; kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value); kv.value = std::forward<V>(value);
} }

View File

@@ -5,9 +5,6 @@
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/util.h" #include "esphome/core/util.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
namespace esphome::api { namespace esphome::api {
@@ -73,9 +70,6 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel, LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse) ListEntitiesAlarmControlPanelResponse)
#endif #endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif #endif
@@ -88,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {} ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) { bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
auto resp = service->encode_list_service_response(); auto resp = service->encode_list_service_response();
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE); return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);

View File

@@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool on_text_sensor(text_sensor::TextSensor *entity) override; bool on_text_sensor(text_sensor::TextSensor *entity) override;
#endif #endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
bool on_service(UserServiceDescriptor *service) override; bool on_service(UserServiceDescriptor *service) override;
#endif #endif
#ifdef USE_CAMERA #ifdef USE_CAMERA
@@ -82,9 +82,6 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *entity) override; bool on_event(event::Event *entity) override;
#endif #endif

View File

@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) { void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force); this->encode_uint64(field_id, encode_zigzag64(value), force);
} }
void encode_message(uint32_t field_id, const ProtoMessage &value); void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
std::vector<uint8_t> *get_buffer() const { return buffer_; } std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected: protected:
@@ -795,7 +795,7 @@ class ProtoSize {
}; };
// Implementation of encode_message - must be after ProtoMessage is defined // Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) { inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first // Calculate the message size first
@@ -846,7 +846,7 @@ class ProtoService {
*/ */
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size // Optimized method that pre-allocates buffer based on message size
bool send_message_(const ProtoMessage &msg, uint8_t message_type) { bool send_message_(const ProtoMessage &msg, uint8_t message_type) {

View File

@@ -60,9 +60,6 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel) INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif #endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity) INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif #endif

View File

@@ -76,9 +76,6 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override; bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif #endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT #ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; }; bool on_event(event::Event *event) override { return true; };
#endif #endif

View File

@@ -11,58 +11,23 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
} }
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; } template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; } template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) { template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
std::vector<bool> result; return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
result.reserve(arg.bool_array.size());
result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
return result;
} }
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) { template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
std::vector<int32_t> result; return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
result.reserve(arg.int_array.size());
result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
return result;
} }
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) { template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
std::vector<float> result; return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
result.reserve(arg.float_array.size());
result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
return result;
} }
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) { template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
std::vector<std::string> result; return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
result.reserve(arg.string_array.size());
result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
return result;
}
// New FixedVector const reference versions for YAML-generated services - zero-copy
template<>
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
return arg.bool_array;
}
template<>
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
return arg.int_array;
}
template<>
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
return arg.float_array;
}
template<>
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
const ExecuteServiceArgument &arg) {
return arg.string_array;
} }
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; } template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; } template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; } template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; } template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; } template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() { template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
return enums::SERVICE_ARG_TYPE_INT_ARRAY; return enums::SERVICE_ARG_TYPE_INT_ARRAY;
@@ -74,18 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
return enums::SERVICE_ARG_TYPE_STRING_ARRAY; return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
} }
// New FixedVector const reference versions for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
return enums::SERVICE_ARG_TYPE_INT_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
}
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
}
} // namespace esphome::api } // namespace esphome::api

View File

@@ -1,31 +1,20 @@
#pragma once #pragma once
#include <tuple>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include "api_pb2.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON #include "esphome/core/automation.h"
#include "esphome/components/json/json_util.h" #include "api_pb2.h"
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS #ifdef USE_API_SERVICES
namespace esphome::api { namespace esphome::api {
// Forward declaration - full definition in api_server.h
class APIServer;
class UserServiceDescriptor { class UserServiceDescriptor {
public: public:
virtual ListEntitiesServicesResponse encode_list_service_response() = 0; virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 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; } bool is_internal() { return false; }
}; };
@@ -34,83 +23,17 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type(); template<typename T> enums::ServiceArgType to_service_arg_type();
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor { template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public: public:
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names, UserServiceBase(std::string name, const std::array<std::string, 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);
}
ListEntitiesServicesResponse encode_list_service_response() override {
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++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.set_name(StringRef(this->arg_names_[i]));
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
#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(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)
// Stores copies of runtime-generated names
template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor {
public:
UserServiceDynamic(std::string name, const std::array<std::string, sizeof...(Ts)> &arg_names)
: name_(std::move(name)), arg_names_(arg_names) { : name_(std::move(name)), arg_names_(arg_names) {
this->key_ = fnv1_hash(this->name_.c_str()); this->key_ = fnv1_hash(this->name_);
} }
ListEntitiesServicesResponse encode_list_service_response() override { ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg; ListEntitiesServicesResponse msg;
msg.set_name(StringRef(this->name_)); msg.set_name(StringRef(this->name_));
msg.key = this->key_; 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>()...}; std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts)); msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) { for (size_t i = 0; i < sizeof...(Ts); i++) {
@@ -124,184 +47,31 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
bool execute_service(const ExecuteServiceRequest &req) override { bool execute_service(const ExecuteServiceRequest &req) override {
if (req.key != this->key_) if (req.key != this->key_)
return false; return false;
if (req.args.size() != sizeof...(Ts)) if (req.args.size() != this->arg_names_.size())
return false; return false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
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; 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: protected:
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0; virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, size_t... S> template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) { this->execute((get_execute_arg_value<Ts>(args[S]))...);
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
} }
// Heap-allocated strings for runtime-generated names
std::string name_; std::string name_;
std::array<std::string, sizeof...(Ts)> arg_names_;
uint32_t key_{0}; uint32_t key_{0};
std::array<std::string, sizeof...(Ts)> arg_names_;
}; };
// Primary template declaration template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
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: public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names) UserServiceTrigger(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {} : UserServiceBase<Ts...>(name, arg_names) {}
protected: protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); } void execute(Ts... x) override { this->trigger(x...); } // NOLINT
};
// 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 } // namespace esphome::api
#endif // USE_API_USER_DEFINED_ACTIONS #endif // USE_API_SERVICES
#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

View File

@@ -1,14 +0,0 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -10,7 +10,7 @@ namespace at581x {
template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XResetAction : public Action<Ts...>, public Parented<AT581XComponent> {
public: public:
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); } void play(Ts... x) { this->parent_->reset_hardware_frontend(); }
}; };
template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> { template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, public Parented<AT581XComponent> {
@@ -25,7 +25,7 @@ template<typename... Ts> class AT581XSettingsAction : public Action<Ts...>, publ
TEMPLATABLE_VALUE(int, trigger_keep) TEMPLATABLE_VALUE(int, trigger_keep)
TEMPLATABLE_VALUE(int, stage_gain) TEMPLATABLE_VALUE(int, stage_gain)
void play(const Ts &...x) { void play(Ts... x) {
if (this->frequency_.has_value()) { if (this->frequency_.has_value()) {
int v = this->frequency_.value(x...); int v = this->frequency_.value(x...);
this->parent_->set_frequency(v); this->parent_->set_frequency(v);

View File

@@ -13,7 +13,7 @@ template<typename... Ts> class SetMicGainAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, mic_gain) TEMPLATABLE_VALUE(float, mic_gain)
void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); } void play(Ts... x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
protected: protected:
AudioAdc *audio_adc_; AudioAdc *audio_adc_;

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class MuteOffAction : public Action<Ts...> {
public: public:
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); } void play(Ts... x) override { this->audio_dac_->set_mute_off(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -21,7 +21,7 @@ template<typename... Ts> class MuteOnAction : public Action<Ts...> {
public: public:
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {} explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); } void play(Ts... x) override { this->audio_dac_->set_mute_on(); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;
@@ -33,7 +33,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, volume) TEMPLATABLE_VALUE(float, volume)
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); } void play(Ts... x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
protected: protected:
AudioDac *audio_dac_; AudioDac *audio_dac_;

View File

@@ -6,9 +6,6 @@ namespace bang_bang {
static const char *const TAG = "bang_bang.climate"; static const char *const TAG = "bang_bang.climate";
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::setup() { void BangBangClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) { this->sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state; this->current_temperature = state;
@@ -34,63 +31,53 @@ void BangBangClimate::setup() {
restore->to_call(this).perform(); restore->to_call(this).perform();
} else { } else {
// restore from defaults, change_away handles those for us // restore from defaults, change_away handles those for us
if (this->supports_cool_ && this->supports_heat_) { if (supports_cool_ && supports_heat_) {
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
} else if (this->supports_cool_) { } else if (supports_cool_) {
this->mode = climate::CLIMATE_MODE_COOL; this->mode = climate::CLIMATE_MODE_COOL;
} else if (this->supports_heat_) { } else if (supports_heat_) {
this->mode = climate::CLIMATE_MODE_HEAT; this->mode = climate::CLIMATE_MODE_HEAT;
} }
this->change_away_(false); this->change_away_(false);
} }
} }
void BangBangClimate::control(const climate::ClimateCall &call) { void BangBangClimate::control(const climate::ClimateCall &call) {
if (call.get_mode().has_value()) { if (call.get_mode().has_value())
this->mode = *call.get_mode(); this->mode = *call.get_mode();
} if (call.get_target_temperature_low().has_value())
if (call.get_target_temperature_low().has_value()) {
this->target_temperature_low = *call.get_target_temperature_low(); this->target_temperature_low = *call.get_target_temperature_low();
} if (call.get_target_temperature_high().has_value())
if (call.get_target_temperature_high().has_value()) {
this->target_temperature_high = *call.get_target_temperature_high(); this->target_temperature_high = *call.get_target_temperature_high();
} if (call.get_preset().has_value())
if (call.get_preset().has_value()) {
this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY); this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
}
this->compute_state_(); this->compute_state_();
this->publish_state(); this->publish_state();
} }
climate::ClimateTraits BangBangClimate::traits() { climate::ClimateTraits BangBangClimate::traits() {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | traits.set_supports_current_temperature(true);
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); if (this->humidity_sensor_ != nullptr)
if (this->humidity_sensor_ != nullptr) { traits.set_supports_current_humidity(true);
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
}
traits.set_supported_modes({ traits.set_supported_modes({
climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_OFF,
}); });
if (this->supports_cool_) { if (supports_cool_)
traits.add_supported_mode(climate::CLIMATE_MODE_COOL); traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
} if (supports_heat_)
if (this->supports_heat_) {
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT); traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
} if (supports_cool_ && supports_heat_)
if (this->supports_cool_ && this->supports_heat_) {
traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL); traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
} traits.set_supports_two_point_target_temperature(true);
if (this->supports_away_) { if (supports_away_) {
traits.set_supported_presets({ traits.set_supported_presets({
climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_HOME,
climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_AWAY,
}); });
} }
traits.set_supports_action(true);
return traits; return traits;
} }
void BangBangClimate::compute_state_() { void BangBangClimate::compute_state_() {
if (this->mode == climate::CLIMATE_MODE_OFF) { if (this->mode == climate::CLIMATE_MODE_OFF) {
this->switch_to_action_(climate::CLIMATE_ACTION_OFF); this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
@@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() {
this->switch_to_action_(target_action); this->switch_to_action_(target_action);
} }
void BangBangClimate::switch_to_action_(climate::ClimateAction action) { void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
if (action == this->action) { if (action == this->action) {
// already in target mode // already in target mode
@@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
this->prev_trigger_ = trig; this->prev_trigger_ = trig;
this->publish_state(); this->publish_state();
} }
void BangBangClimate::change_away_(bool away) { void BangBangClimate::change_away_(bool away) {
if (!away) { if (!away) {
this->target_temperature_low = this->normal_config_.default_temperature_low; this->target_temperature_low = this->normal_config_.default_temperature_low;
@@ -191,26 +176,22 @@ void BangBangClimate::change_away_(bool away) {
} }
this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME; this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
} }
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) { void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
this->normal_config_ = normal_config; this->normal_config_ = normal_config;
} }
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) { void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
this->supports_away_ = true; this->supports_away_ = true;
this->away_config_ = away_config; this->away_config_ = away_config;
} }
BangBangClimate::BangBangClimate()
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void BangBangClimate::dump_config() { void BangBangClimate::dump_config() {
LOG_CLIMATE("", "Bang Bang Climate", this); LOG_CLIMATE("", "Bang Bang Climate", this);
ESP_LOGCONFIG(TAG, ESP_LOGCONFIG(TAG,

View File

@@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component {
void set_sensor(sensor::Sensor *sensor); void set_sensor(sensor::Sensor *sensor);
void set_humidity_sensor(sensor::Sensor *humidity_sensor); void set_humidity_sensor(sensor::Sensor *humidity_sensor);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
void set_supports_cool(bool supports_cool); void set_supports_cool(bool supports_cool);
Trigger<> *get_heat_trigger() const;
void set_supports_heat(bool supports_heat); void set_supports_heat(bool supports_heat);
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
void set_away_config(const BangBangClimateTargetTempConfig &away_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config);
Trigger<> *get_idle_trigger() const;
Trigger<> *get_cool_trigger() const;
Trigger<> *get_heat_trigger() const;
protected: protected:
/// Override control to change settings of the climate device. /// Override control to change settings of the climate device.
void control(const climate::ClimateCall &call) override; void control(const climate::ClimateCall &call) override;
@@ -57,10 +56,16 @@ class BangBangClimate : public climate::Climate, public Component {
* *
* In idle mode, the controller is assumed to have both heating and cooling disabled. * In idle mode, the controller is assumed to have both heating and cooling disabled.
*/ */
Trigger<> *idle_trigger_{nullptr}; Trigger<> *idle_trigger_;
/** The trigger to call when the controller should switch to cooling mode. /** The trigger to call when the controller should switch to cooling mode.
*/ */
Trigger<> *cool_trigger_{nullptr}; Trigger<> *cool_trigger_;
/** Whether the controller supports cooling.
*
* A false value for this attribute means that the controller has no cooling action
* (for example a thermostat, where only heating and not-heating is possible).
*/
bool supports_cool_{false};
/** The trigger to call when the controller should switch to heating mode. /** The trigger to call when the controller should switch to heating mode.
* *
* A null value for this attribute means that the controller has no heating action * A null value for this attribute means that the controller has no heating action
@@ -68,23 +73,15 @@ class BangBangClimate : public climate::Climate, public Component {
* (blinds open) is possible. * (blinds open) is possible.
*/ */
Trigger<> *heat_trigger_{nullptr}; Trigger<> *heat_trigger_{nullptr};
bool supports_heat_{false};
/** A reference to the trigger that was previously active. /** A reference to the trigger that was previously active.
* *
* This is so that the previous trigger can be stopped before enabling a new one. * This is so that the previous trigger can be stopped before enabling a new one.
*/ */
Trigger<> *prev_trigger_{nullptr}; Trigger<> *prev_trigger_{nullptr};
/** Whether the controller supports cooling/heating
*
* A false value for this attribute means that the controller has no respective action
* (for example a thermostat, where only heating and not-heating is possible).
*/
bool supports_cool_{false};
bool supports_heat_{false};
bool supports_away_{false};
BangBangClimateTargetTempConfig normal_config_{}; BangBangClimateTargetTempConfig normal_config_{};
bool supports_away_{false};
BangBangClimateTargetTempConfig away_config_{}; BangBangClimateTargetTempConfig away_config_{};
}; };

View File

@@ -99,7 +99,9 @@ enum BedjetCommand : uint8_t {
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

View File

@@ -1,7 +1,12 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_HEAT_MODE, CONF_TEMPERATURE_SOURCE from esphome.const import (
CONF_HEAT_MODE,
CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID,
)
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
@@ -33,6 +38,22 @@ CONFIG_SCHEMA = (
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(
# TODO: remove compat layer.
{
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"
),
cv.Optional(CONF_TIME_ID): cv.invalid(
"The 'time_id' option has been moved to the `bedjet` component."
),
cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid(
"The 'receive_timeout' option has been moved to the `bedjet` component."
),
}
)
.extend(BEDJET_CLIENT_SCHEMA) .extend(BEDJET_CLIENT_SCHEMA)
) )

View File

@@ -8,15 +8,15 @@ namespace bedjet {
using namespace esphome::climate; using namespace esphome::climate;
static const char *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) {
if (fan_step < BEDJET_FAN_SPEED_COUNT) if (fan_step < BEDJET_FAN_SPEED_COUNT)
return BEDJET_FAN_STEP_NAMES[fan_step]; return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step];
return nullptr; return nullptr;
} }
static uint8_t bedjet_fan_speed_to_step(const char *fan_step_percent) { static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) {
for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) {
if (strcmp(BEDJET_FAN_STEP_NAMES[i], fan_step_percent) == 0) { if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) {
return i; return i;
} }
} }
@@ -48,7 +48,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode)));
} }
for (const auto &mode : traits.get_supported_custom_fan_modes()) { for (const auto &mode : traits.get_supported_custom_fan_modes()) {
ESP_LOGCONFIG(TAG, " - %s (c)", mode); ESP_LOGCONFIG(TAG, " - %s (c)", mode.c_str());
} }
ESP_LOGCONFIG(TAG, " Supported presets:"); ESP_LOGCONFIG(TAG, " Supported presets:");
@@ -56,7 +56,7 @@ void BedJetClimate::dump_config() {
ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset))); ESP_LOGCONFIG(TAG, " - %s", LOG_STR_ARG(climate_preset_to_string(preset)));
} }
for (const auto &preset : traits.get_supported_custom_presets()) { for (const auto &preset : traits.get_supported_custom_presets()) {
ESP_LOGCONFIG(TAG, " - %s (c)", preset); ESP_LOGCONFIG(TAG, " - %s (c)", preset.c_str());
} }
} }
@@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() {
this->target_temperature = NAN; this->target_temperature = NAN;
this->current_temperature = NAN; this->current_temperature = NAN;
this->preset.reset(); this->preset.reset();
this->clear_custom_preset_(); this->custom_preset.reset();
this->publish_state(); this->publish_state();
} }
@@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) {
if (button_result) { if (button_result) {
this->mode = mode; this->mode = mode;
// We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
} }
} }
@@ -144,7 +144,8 @@ void BedJetClimate::control(const ClimateCall &call) {
if (result) { if (result) {
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->set_preset_(CLIMATE_PRESET_BOOST); this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
} }
} else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) {
if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) {
@@ -152,7 +153,7 @@ void BedJetClimate::control(const ClimateCall &call) {
result = this->parent_->send_button(heat_button(this->heating_mode_)); result = this->parent_->send_button(heat_button(this->heating_mode_));
if (result) { if (result) {
this->preset.reset(); this->preset.reset();
this->clear_custom_preset_(); this->custom_preset.reset();
} }
} else { } else {
ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'",
@@ -163,27 +164,28 @@ void BedJetClimate::control(const ClimateCall &call) {
ESP_LOGW(TAG, "Unsupported preset: %d", preset); ESP_LOGW(TAG, "Unsupported preset: %d", preset);
return; return;
} }
} else if (call.has_custom_preset()) { } else if (call.get_custom_preset().has_value()) {
const char *preset = call.get_custom_preset(); std::string preset = *call.get_custom_preset();
bool result; bool result;
if (strcmp(preset, "M1") == 0) { if (preset == "M1") {
result = this->parent_->button_memory1(); result = this->parent_->button_memory1();
} else if (strcmp(preset, "M2") == 0) { } else if (preset == "M2") {
result = this->parent_->button_memory2(); result = this->parent_->button_memory2();
} else if (strcmp(preset, "M3") == 0) { } else if (preset == "M3") {
result = this->parent_->button_memory3(); result = this->parent_->button_memory3();
} else if (strcmp(preset, "LTD HT") == 0) { } else if (preset == "LTD HT") {
result = this->parent_->button_heat(); result = this->parent_->button_heat();
} else if (strcmp(preset, "EXT HT") == 0) { } else if (preset == "EXT HT") {
result = this->parent_->button_ext_heat(); result = this->parent_->button_ext_heat();
} else { } else {
ESP_LOGW(TAG, "Unsupported preset: %s", preset); ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str());
return; return;
} }
if (result) { if (result) {
this->set_custom_preset_(preset); this->custom_preset = preset;
this->preset.reset();
} }
} }
@@ -205,16 +207,19 @@ void BedJetClimate::control(const ClimateCall &call) {
} }
if (result) { if (result) {
this->set_fan_mode_(fan_mode); this->fan_mode = fan_mode;
this->custom_fan_mode.reset();
} }
} else if (call.has_custom_fan_mode()) { } else if (call.get_custom_fan_mode().has_value()) {
const char *fan_mode = call.get_custom_fan_mode(); auto fan_mode = *call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode); auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) { if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index); ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(),
fan_index);
bool result = this->parent_->set_fan_index(fan_index); bool result = this->parent_->set_fan_index(fan_index);
if (result) { if (result) {
this->set_custom_fan_mode_(fan_mode); this->custom_fan_mode = fan_mode;
this->fan_mode.reset();
} }
} }
} }
@@ -240,7 +245,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step);
if (fan_mode_name != nullptr) { if (fan_mode_name != nullptr) {
this->set_custom_fan_mode_(fan_mode_name); this->custom_fan_mode = *fan_mode_name;
} }
// TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any.
@@ -250,7 +255,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->mode = CLIMATE_MODE_OFF; this->mode = CLIMATE_MODE_OFF;
this->action = CLIMATE_ACTION_IDLE; this->action = CLIMATE_ACTION_IDLE;
this->fan_mode = CLIMATE_FAN_OFF; this->fan_mode = CLIMATE_FAN_OFF;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
@@ -261,7 +266,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->set_custom_preset_("LTD HT"); this->set_custom_preset_("LTD HT");
} else { } else {
this->clear_custom_preset_(); this->custom_preset.reset();
} }
break; break;
@@ -270,7 +275,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
this->preset.reset(); this->preset.reset();
if (this->heating_mode_ == HEAT_MODE_EXTENDED) { if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
this->clear_custom_preset_(); this->custom_preset.reset();
} else { } else {
this->set_custom_preset_("EXT HT"); this->set_custom_preset_("EXT HT");
} }
@@ -279,19 +284,20 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) {
case MODE_COOL: case MODE_COOL:
this->mode = CLIMATE_MODE_FAN_ONLY; this->mode = CLIMATE_MODE_FAN_ONLY;
this->action = CLIMATE_ACTION_COOLING; this->action = CLIMATE_ACTION_COOLING;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_DRY: case MODE_DRY:
this->mode = CLIMATE_MODE_DRY; this->mode = CLIMATE_MODE_DRY;
this->action = CLIMATE_ACTION_DRYING; this->action = CLIMATE_ACTION_DRYING;
this->clear_custom_preset_(); this->custom_preset.reset();
this->preset.reset(); this->preset.reset();
break; break;
case MODE_TURBO: case MODE_TURBO:
this->set_preset_(CLIMATE_PRESET_BOOST); this->preset = CLIMATE_PRESET_BOOST;
this->custom_preset.reset();
this->mode = CLIMATE_MODE_HEAT; this->mode = CLIMATE_MODE_HEAT;
this->action = CLIMATE_ACTION_HEATING; this->action = CLIMATE_ACTION_HEATING;
break; break;

View File

@@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
climate::ClimateTraits traits() override { climate::ClimateTraits traits() override {
auto traits = climate::ClimateTraits(); auto traits = climate::ClimateTraits();
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); traits.set_supports_action(true);
traits.set_supports_current_temperature(true);
traits.set_supported_modes({ traits.set_supported_modes({
climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_OFF,
climate::CLIMATE_MODE_HEAT, climate::CLIMATE_MODE_HEAT,
@@ -43,20 +44,28 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
}); });
// It would be better if we had a slider for the fan modes. // It would be better if we had a slider for the fan modes.
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES); traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
traits.set_supported_presets({ traits.set_supported_presets({
// If we support NONE, then have to decide what happens if the user switches to it (turn off?) // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
// climate::CLIMATE_PRESET_NONE, // climate::CLIMATE_PRESET_NONE,
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
climate::CLIMATE_PRESET_BOOST, climate::CLIMATE_PRESET_BOOST,
}); });
// String literals are stored in rodata and valid for program lifetime
traits.set_supported_custom_presets({ traits.set_supported_custom_presets({
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", // We could fetch biodata from bedjet and set these names that way.
// But then we have to invert the lookup in order to send the right preset.
// For now, we can leave them as M1-3 to match the remote buttons.
// EXT HT added to match remote button.
"EXT HT",
"M1", "M1",
"M2", "M2",
"M3", "M3",
}); });
if (this->heating_mode_ == HEAT_MODE_EXTENDED) {
traits.add_supported_custom_preset("LTD HT");
} else {
traits.add_supported_custom_preset("EXT HT");
}
traits.set_visual_min_temperature(19.0); traits.set_visual_min_temperature(19.0);
traits.set_visual_max_temperature(43.0); traits.set_visual_max_temperature(43.0);
traits.set_visual_temperature_step(1.0); traits.set_visual_temperature_step(1.0);

View File

@@ -1,8 +1,8 @@
#include "bh1750.h" #include "bh1750.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bh1750 { namespace esphome {
namespace bh1750 {
static const char *const TAG = "bh1750.sensor"; static const char *const TAG = "bh1750.sensor";
@@ -13,31 +13,6 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000; static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001; static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
// Measurement time constants (datasheet values)
static constexpr uint16_t MTREG_DEFAULT = 69;
static constexpr uint16_t MTREG_MIN = 31;
static constexpr uint16_t MTREG_MAX = 254;
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
// Conversion constants (datasheet formulas)
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
// MTreg calculation constants
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
static constexpr int COUNTS_NUMERATOR = 10;
static constexpr int COUNTS_DENOMINATOR = 12;
// MTreg register bit manipulation constants
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
/* /*
bh1750 properties: bh1750 properties:
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
this->mark_failed(); this->mark_failed();
return; return;
} }
this->state_ = IDLE; }
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
if (active_mtreg_ != mtreg) {
// set mtreg
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
}
active_mtreg_ = mtreg;
}
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = 24 * mtreg / 69;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = 180 * mtreg / 69;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = 180 * mtreg / 69;
break;
default:
f(NAN);
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
// probably not needed, but adjust for rounding
meas_time++;
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / 1.2f;
lx *= 69.0f / mtreg;
if (mode == BH1750_MODE_H2)
lx /= 2.0f;
f(lx);
});
} }
void BH1750Sensor::dump_config() { void BH1750Sensor::dump_config() {
@@ -82,189 +124,45 @@ void BH1750Sensor::dump_config() {
} }
void BH1750Sensor::update() { void BH1750Sensor::update() {
const uint32_t now = millis(); // first do a quick measurement in L-mode with full range
// to find right range
// Start coarse measurement to determine optimal mode/mtreg this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
if (this->state_ != IDLE) { if (std::isnan(val)) {
// Safety timeout: reset if stuck this->status_set_warning();
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) { this->publish_state(NAN);
ESP_LOGW(TAG, "Measurement timeout, resetting state");
this->state_ = IDLE;
} else {
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
return; return;
} }
}
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) { BH1750Mode use_mode;
this->status_set_warning(); uint8_t use_mtreg;
this->publish_state(NAN); if (val <= 7000) {
return; use_mode = BH1750_MODE_H2;
} use_mtreg = 254;
} else {
this->state_ = WAITING_COARSE_MEASUREMENT; use_mode = BH1750_MODE_H;
this->enable_loop(); // Enable loop while measurement in progress // lx = counts / 1.2 * (69 / mtreg)
} // -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
void BH1750Sensor::loop() { // -> mtreg = 50000*(10/12)*(69/lx)
const uint32_t now = App.get_loop_component_start_time(); int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
switch (this->state_) {
case IDLE:
// Disable loop when idle to save cycles
this->disable_loop();
break;
case WAITING_COARSE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_COARSE_RESULT;
}
break;
case READING_COARSE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
this->process_coarse_result_(lx);
// Start fine measurement with optimal settings
// fetch millis() again since the read can take a bit
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, millis())) {
this->fail_and_reset_();
break;
}
this->state_ = WAITING_FINE_MEASUREMENT;
break;
} }
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
case WAITING_FINE_MEASUREMENT: this->read_lx_(use_mode, use_mtreg, [this](float val) {
if (now - this->measurement_start_time_ >= this->measurement_duration_) { if (std::isnan(val)) {
this->state_ = READING_FINE_RESULT; this->status_set_warning();
this->publish_state(NAN);
return;
} }
break; ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
case READING_FINE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
this->status_clear_warning(); this->status_clear_warning();
this->publish_state(lx); this->publish_state(val);
this->state_ = IDLE; });
break; });
}
}
}
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
// Power on
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
return false;
}
// Set MTreg if changed
if (this->active_mtreg_ != mtreg) {
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
this->active_mtreg_ = 0;
return false;
}
this->active_mtreg_ = mtreg;
}
// Start measurement
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
default:
return false;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
return false;
}
// Store current measurement parameters
this->current_mode_ = mode;
this->current_mtreg_ = mtreg;
this->measurement_start_time_ = now;
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
return true;
}
bool BH1750Sensor::read_measurement_(float &lx_out) {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
return false;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / RESOLUTION_DIVISOR;
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
if (this->current_mode_ == BH1750_MODE_H2) {
lx /= MODE_H2_DIVISOR;
}
lx_out = lx;
return true;
}
void BH1750Sensor::process_coarse_result_(float lx) {
if (std::isnan(lx)) {
// Use defaults if coarse measurement failed
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
return;
}
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
} else {
this->fine_mode_ = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
}
void BH1750Sensor::fail_and_reset_() {
this->status_set_warning();
this->publish_state(NAN);
this->state_ = IDLE;
} }
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace esphome::bh1750 } // namespace bh1750
} // namespace esphome

View File

@@ -4,9 +4,10 @@
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h" #include "esphome/components/i2c/i2c.h"
namespace esphome::bh1750 { namespace esphome {
namespace bh1750 {
enum BH1750Mode : uint8_t { enum BH1750Mode {
BH1750_MODE_L, BH1750_MODE_L,
BH1750_MODE_H, BH1750_MODE_H,
BH1750_MODE_H2, BH1750_MODE_H2,
@@ -20,36 +21,13 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
void update() override; void update() override;
void loop() override;
float get_setup_priority() const override; float get_setup_priority() const override;
protected: protected:
// State machine states void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
enum State : uint8_t {
IDLE,
WAITING_COARSE_MEASUREMENT,
READING_COARSE_RESULT,
WAITING_FINE_MEASUREMENT,
READING_FINE_RESULT,
};
// 4-byte aligned members
uint32_t measurement_start_time_{0};
uint32_t measurement_duration_{0};
// 1-byte members grouped together to minimize padding
State state_{IDLE};
BH1750Mode current_mode_{BH1750_MODE_L};
uint8_t current_mtreg_{31};
BH1750Mode fine_mode_{BH1750_MODE_H2};
uint8_t fine_mtreg_{254};
uint8_t active_mtreg_{0}; uint8_t active_mtreg_{0};
// Helper methods
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
bool read_measurement_(float &lx_out);
void process_coarse_result_(float lx);
void fail_and_reset_();
}; };
} // namespace esphome::bh1750 } // namespace bh1750
} // namespace esphome

View File

@@ -20,6 +20,16 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_ILLUMINANCE, device_class=DEVICE_CLASS_ILLUMINANCE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
) )
.extend(
{
cv.Optional("resolution"): cv.invalid(
"The 'resolution' option has been removed. The optimal value is now dynamically calculated."
),
cv.Optional("measurement_duration"): cv.invalid(
"The 'measurement_duration' option has been removed. The optimal value is now dynamically calculated."
),
}
)
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(i2c.i2c_device_schema(0x23)) .extend(i2c.i2c_device_schema(0x23))
) )

View File

@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
i2c::ErrorCode result_code = i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) { if (result_code != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL)); this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
return; return;
} }
} }

View File

@@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter) InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component) AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter) LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component) SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__) _LOGGER = getLogger(__name__)
@@ -265,31 +264,20 @@ async def delayed_off_filter_to_code(config, filter_id):
), ),
) )
async def autorepeat_filter_to_code(config, filter_id): async def autorepeat_filter_to_code(config, filter_id):
timings = []
if len(config) > 0: if len(config) > 0:
timings = [ timings.extend(
cg.StructInitializer( (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
("delay", conf[CONF_DELAY]),
("time_off", conf[CONF_TIME_OFF]),
("time_on", conf[CONF_TIME_ON]),
)
for conf in config for conf in config
] )
else: else:
timings = [ timings.append(
cg.StructInitializer( (
cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"), cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds), cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
( cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
"time_off",
cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
),
(
"time_on",
cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
),
) )
] )
var = cg.new_Pvariable(filter_id, timings) var = cg.new_Pvariable(filter_id, timings)
await cg.register_component(var, {}) await cg.register_component(var, {})
return var return var
@@ -300,7 +288,7 @@ async def lambda_filter_to_code(config, filter_id):
lambda_ = await cg.process_lambda( lambda_ = await cg.process_lambda(
config, [(bool, "x")], return_type=cg.optional.template(bool) config, [(bool, "x")], return_type=cg.optional.template(bool)
) )
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter) return cg.new_Pvariable(filter_id, lambda_)
@register_filter( @register_filter(
@@ -548,6 +536,11 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema) return _BINARY_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):
await setup_entity(var, config, "binary_sensor") await setup_entity(var, config, "binary_sensor")

View File

@@ -1,11 +1,12 @@
#include "automation.h" #include "automation.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::binary_sensor { namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor.automation"; static const char *const TAG = "binary_sensor.automation";
void MultiClickTrigger::on_state_(bool state) { void binary_sensor::MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events // Handle duplicate events
if (state == this->last_state_) { if (state == this->last_state_) {
return; return;
@@ -66,7 +67,7 @@ void MultiClickTrigger::on_state_(bool state) {
*this->at_index_ = *this->at_index_ + 1; *this->at_index_ = *this->at_index_ + 1;
} }
void MultiClickTrigger::schedule_cooldown_() { void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_); ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true; this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() { this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
@@ -78,7 +79,7 @@ void MultiClickTrigger::schedule_cooldown_() {
this->cancel_timeout("is_valid"); this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid"); this->cancel_timeout("is_not_valid");
} }
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) { void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) { if (min_length == 0) {
this->is_valid_ = true; this->is_valid_ = true;
return; return;
@@ -89,19 +90,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
this->is_valid_ = true; this->is_valid_ = true;
}); });
} }
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) { void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() { 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"); ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false; this->is_valid_ = false;
this->schedule_cooldown_(); this->schedule_cooldown_();
}); });
} }
void MultiClickTrigger::cancel() { void binary_sensor::MultiClickTrigger::cancel() {
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled."); ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
this->is_valid_ = false; this->is_valid_ = false;
this->schedule_cooldown_(); this->schedule_cooldown_();
} }
void MultiClickTrigger::trigger_() { void binary_sensor::MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!"); ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset(); this->at_index_.reset();
this->cancel_timeout("trigger"); this->cancel_timeout("trigger");
@@ -117,4 +118,5 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length; return length >= min_length && length <= max_length;
} }
} }
} // namespace esphome::binary_sensor } // namespace binary_sensor
} // namespace esphome

View File

@@ -2,14 +2,15 @@
#include <cinttypes> #include <cinttypes>
#include <utility> #include <utility>
#include <vector>
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h" #include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::binary_sensor { namespace esphome {
namespace binary_sensor {
struct MultiClickTriggerEvent { struct MultiClickTriggerEvent {
bool state; bool state;
@@ -91,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
class MultiClickTrigger : public Trigger<>, public Component { class MultiClickTrigger : public Trigger<>, public Component {
public: public:
explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing)
: parent_(parent), timing_(timing) {} : parent_(parent), timing_(std::move(timing)) {}
void setup() override { void setup() override {
this->last_state_ = this->parent_->get_state_default(false); this->last_state_ = this->parent_->get_state_default(false);
@@ -114,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
void trigger_(); void trigger_();
BinarySensor *parent_; BinarySensor *parent_;
FixedVector<MultiClickTriggerEvent> timing_; std::vector<MultiClickTriggerEvent> timing_;
uint32_t invalid_cooldown_{1000}; uint32_t invalid_cooldown_{1000};
optional<size_t> at_index_{}; optional<size_t> at_index_{};
bool last_state_{false}; bool last_state_{false};
@@ -140,7 +141,7 @@ class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> { template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public: public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {} BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
bool check(const Ts &...x) override { return this->parent_->state == this->state_; } bool check(Ts... x) override { return this->parent_->state == this->state_; }
protected: protected:
BinarySensor *parent_; BinarySensor *parent_;
@@ -152,7 +153,7 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
TEMPLATABLE_VALUE(bool, state) TEMPLATABLE_VALUE(bool, state)
void play(const Ts &...x) override { void play(Ts... x) override {
auto val = this->state_.value(x...); auto val = this->state_.value(x...);
this->sensor_->publish_state(val); this->sensor_->publish_state(val);
} }
@@ -165,10 +166,11 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
public: public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {} explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(const Ts &...x) override { this->sensor_->invalidate_state(); } void play(Ts... x) override { this->sensor_->invalidate_state(); }
protected: protected:
BinarySensor *sensor_; BinarySensor *sensor_;
}; };
} // namespace esphome::binary_sensor } // namespace binary_sensor
} // namespace esphome

View File

@@ -1,9 +1,9 @@
#include "binary_sensor.h" #include "binary_sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::binary_sensor { namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor"; static const char *const TAG = "binary_sensor";
@@ -34,20 +34,10 @@ void BinarySensor::publish_initial_state(bool new_state) {
void BinarySensor::send_state_internal(bool new_state) { void BinarySensor::send_state_internal(bool new_state) {
// copy the new state to the visible property for backwards compatibility, before any callbacks // copy the new state to the visible property for backwards compatibility, before any callbacks
this->state = new_state; this->state = new_state;
// Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed // Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
this->set_new_state(new_state); if (this->set_state_(new_state)) {
} ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
if (StatefulEntityBase::set_new_state(new_state)) {
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
} }
return false;
} }
void BinarySensor::add_filter(Filter *filter) { void BinarySensor::add_filter(Filter *filter) {
@@ -61,11 +51,13 @@ void BinarySensor::add_filter(Filter *filter) {
last_filter->next_ = filter; last_filter->next_ = filter;
} }
} }
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) { void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
for (Filter *filter : filters) { for (Filter *filter : filters) {
this->add_filter(filter); this->add_filter(filter);
} }
} }
bool BinarySensor::is_status_binary_sensor() const { return false; } bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace esphome::binary_sensor } // namespace binary_sensor
} // namespace esphome

View File

@@ -4,9 +4,11 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h" #include "esphome/components/binary_sensor/filter.h"
#include <initializer_list> #include <vector>
namespace esphome::binary_sensor { namespace esphome {
namespace binary_sensor {
class BinarySensor; class BinarySensor;
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj); void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
@@ -46,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
void publish_initial_state(bool new_state); void publish_initial_state(bool new_state);
void add_filter(Filter *filter); void add_filter(Filter *filter);
void add_filters(std::initializer_list<Filter *> filters); void add_filters(const std::vector<Filter *> &filters);
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
@@ -61,8 +63,6 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
protected: protected:
Filter *filter_list_{nullptr}; Filter *filter_list_{nullptr};
bool set_new_state(const optional<bool> &new_state) override;
}; };
class BinarySensorInitiallyOff : public BinarySensor { class BinarySensorInitiallyOff : public BinarySensor {
@@ -70,4 +70,5 @@ class BinarySensorInitiallyOff : public BinarySensor {
bool has_state() const override { return true; } bool has_state() const override { return true; }
}; };
} // namespace esphome::binary_sensor } // namespace binary_sensor
} // namespace esphome

View File

@@ -1,8 +1,11 @@
#include "filter.h" #include "filter.h"
#include "binary_sensor.h" #include "binary_sensor.h"
#include <utility>
namespace esphome::binary_sensor { namespace esphome {
namespace binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
@@ -65,7 +68,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
optional<bool> InvertFilter::new_value(bool value) { return !value; } optional<bool> InvertFilter::new_value(bool value) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {} AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value) { optional<bool> AutorepeatFilter::new_value(bool value) {
if (value) { if (value) {
@@ -130,4 +133,6 @@ optional<bool> SettleFilter::new_value(bool value) {
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esphome::binary_sensor } // namespace binary_sensor
} // namespace esphome

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