1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-05 09:31:54 +00:00

Compare commits

..

23 Commits

Author SHA1 Message Date
J. Nick Koston
0e137476fb fix 2025-10-28 15:45:07 -05:00
J. Nick Koston
9b9c50994b remove dead code 2025-10-28 15:41:55 -05:00
J. Nick Koston
2b385403f8 remove dead code 2025-10-28 15:40:38 -05:00
J. Nick Koston
c928ccbcc2 remove dead code 2025-10-28 15:39:46 -05:00
J. Nick Koston
75b1936d71 remove dead code 2025-10-28 15:39:14 -05:00
J. Nick Koston
04533bdc5b remove dead code 2025-10-28 15:37:03 -05:00
J. Nick Koston
22be105535 remove dead code 2025-10-28 15:36:08 -05:00
J. Nick Koston
bd52efdc05 remove dead code 2025-10-28 15:34:47 -05:00
J. Nick Koston
175c19f29e remove dead code 2025-10-28 15:33:45 -05:00
J. Nick Koston
b476cba83d remove dead code 2025-10-28 15:32:29 -05:00
J. Nick Koston
fa8da1473f fix 2025-10-28 15:31:02 -05:00
J. Nick Koston
fc2a447da7 fix 2025-10-28 15:26:25 -05:00
J. Nick Koston
f9298aef0f tweak 2025-10-28 15:19:55 -05:00
J. Nick Koston
77e8c12c96 fix 2025-10-28 15:17:18 -05:00
J. Nick Koston
e2d1dc8443 test 2025-10-28 15:08:15 -05:00
J. Nick Koston
0711542ff4 test 2025-10-28 15:03:20 -05:00
J. Nick Koston
fe8a76b6d6 test 2025-10-28 15:02:32 -05:00
J. Nick Koston
ccc895ed81 test 2025-10-28 15:01:03 -05:00
J. Nick Koston
afe628d62c test 2025-10-28 14:59:42 -05:00
J. Nick Koston
10b24a225c test 2025-10-28 14:57:55 -05:00
J. Nick Koston
f44227a578 test 2025-10-28 14:57:31 -05:00
J. Nick Koston
311230492f test 2025-10-28 14:55:25 -05:00
J. Nick Koston
1c114093ec test 2025-10-28 14:50:10 -05:00
480 changed files with 3270 additions and 7503 deletions

View File

@@ -416,7 +416,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;
@@ -430,15 +430,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0); .reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions); const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = 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).`;
} }
@@ -466,8 +466,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)
); );
@@ -627,7 +627,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'];
@@ -636,7 +635,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

@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with: with:
name: generated-proto-files name: generated-proto-files
path: | path: |

View File

@@ -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,9 +123,9 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.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:
@@ -180,7 +180,6 @@ jobs:
memory_impact: ${{ steps.determine.outputs.memory-impact }} memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -192,11 +191,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@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
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:
@@ -220,13 +214,6 @@ jobs:
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
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
@@ -471,7 +458,7 @@ jobs:
GH_TOKEN: ${{ github.token }} GH_TOKEN: ${{ github.token }}
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: 2 max-parallel: 1
matrix: matrix:
include: include:
- id: clang-tidy - id: clang-tidy
@@ -549,18 +536,59 @@ jobs:
run: script/ci-suggest-changes run: script/ci-suggest-changes
if: always() if: always()
test-build-components-splitter:
name: Split components for intelligent grouping (40 weighted per batch)
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
outputs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Split components intelligently based on bus configurations
id: split
run: |
. venv/bin/activate
# Use intelligent splitter that groups components with same bus configs
components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
# Only isolate directly changed components when targeting dev branch
# For beta/release branches, group everything for faster CI
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
directly_changed='[]'
echo "Target branch: ${{ github.base_ref }} - grouping all components"
else
directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
fi
echo "Splitting components intelligently..."
output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
echo "$output" >> $GITHUB_OUTPUT
test-build-components-split: test-build-components-split:
name: Test components batch (${{ matrix.components }}) name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
needs: needs:
- common - common
- determine-jobs - determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0 if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy: strategy:
fail-fast: false fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }} max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix: matrix:
components: ${{ fromJson(needs.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: |
@@ -821,7 +849,7 @@ jobs:
fi fi
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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
@@ -885,7 +913,7 @@ jobs:
--platform "$platform" --platform "$platform"
- name: Upload memory analysis JSON - name: Upload memory analysis JSON
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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
@@ -915,13 +943,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }} python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }} cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON - name: Download target analysis JSON
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: memory-analysis-target name: memory-analysis-target
path: ./memory-analysis path: ./memory-analysis
continue-on-error: true continue-on-error: true
- name: Download PR analysis JSON - name: Download PR analysis JSON
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
name: memory-analysis-pr name: memory-analysis-pr
path: ./memory-analysis path: ./memory-analysis
@@ -952,6 +980,7 @@ jobs:
- clang-tidy-nosplit - clang-tidy-nosplit
- clang-tidy-split - clang-tidy-split
- determine-jobs - determine-jobs
- test-build-components-splitter
- test-build-components-split - test-build-components-split
- pre-commit-ci-lite - pre-commit-ci-lite
- memory-impact-target-branch - memory-impact-target-branch

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2 uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
with: with:
category: "/language:${{matrix.language}}" category: "/language:${{matrix.language}}"

View File

@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.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
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests - name: Download digests
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
with: with:
pattern: digests-* pattern: digests-*
path: /tmp/digests path: /tmp/digests

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.3 rev: v0.14.1
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff

View File

@@ -155,7 +155,6 @@ 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
@@ -181,7 +180,7 @@ 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
@@ -480,7 +479,6 @@ esphome/components/template/fan/* @ssieb
esphome/components/text/* @mauritskorse esphome/components/text/* @mauritskorse
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

View File

@@ -207,14 +207,14 @@ def choose_upload_log_host(
if has_mqtt_logging(): if has_mqtt_logging():
resolved.append("MQTT") resolved.append("MQTT")
if has_api() and has_non_ip_address() 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 +318,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):

View File

@@ -15,7 +15,7 @@ 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 (
LambdaExpression, LambdaExpression,
MockObj, MockObj,
@@ -182,7 +182,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
value = cv.Schema([extra_validators])(value) value = cv.Schema([extra_validators])(value)
if single: if single:
if len(value) != 1: if len(value) != 1:
raise cv.Invalid("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
@@ -310,30 +310,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)
) )

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

@@ -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

@@ -89,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()) {
@@ -109,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()) {
@@ -129,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()) {
@@ -149,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_;
@@ -159,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_;
@@ -169,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_;
@@ -178,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

@@ -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

@@ -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::vector"];
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
@@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse {
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_no_template) = "climate::ClimateFanModeMask"];
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
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::vector"];
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
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;

View File

@@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
} }
if (traits.supports_direction()) if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes() && 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,
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
msg.supports_speed = traits.supports_speed(); msg.supports_speed = traits.supports_speed();
msg.supports_direction = traits.supports_direction(); msg.supports_direction = traits.supports_direction();
msg.supported_speed_count = traits.supported_speed_count(); msg.supported_speed_count = traits.supported_speed_count();
msg.supported_preset_modes = &traits.supported_preset_modes(); 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) {
@@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
} }
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->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);
@@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *select = static_cast<select::Select *>(entity); auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp; SelectStateResponse resp;
resp.set_state(StringRef(select->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);
} }

View File

@@ -142,6 +142,11 @@ APIError APINoiseFrameHelper::loop() {
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/ */
APIError APINoiseFrameHelper::try_read_frame_() { APIError APINoiseFrameHelper::try_read_frame_() {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK)
if (this->rx_buf_len_ == 0) {
this->rx_buf_.clear();
}
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet
@@ -434,7 +439,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());

View File

@@ -54,6 +54,11 @@ APIError APIPlaintextFrameHelper::loop() {
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
*/ */
APIError APIPlaintextFrameHelper::try_read_frame_() { APIError APIPlaintextFrameHelper::try_read_frame_() {
// Clear buffer when starting a new frame (rx_buf_len_ == 0 means not resuming after WOULD_BLOCK)
if (this->rx_buf_len_ == 0) {
this->rx_buf_.clear();
}
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes // Now that we know when the socket is ready, we can read up to 3 bytes
@@ -230,7 +235,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

@@ -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
@@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const {
for (const auto &it : *this->supported_swing_modes) { for (const auto &it : *this->supported_swing_modes) {
buffer.encode_uint32(14, static_cast<uint32_t>(it), true); buffer.encode_uint32(14, static_cast<uint32_t>(it), true);
} }
for (const 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
@@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_fan_modes->empty()) { if (!this->supported_custom_fan_modes->empty()) {
for (const 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()) {
@@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const {
} }
} }
if (!this->supported_custom_presets->empty()) { if (!this->supported_custom_presets->empty()) {
for (const 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);

View File

@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
const std::vector<const char *> *supported_preset_modes{}; const std::vector<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
@@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
bool supports_action{false}; bool supports_action{false};
const climate::ClimateFanModeMask *supported_fan_modes{}; const climate::ClimateFanModeMask *supported_fan_modes{};
const climate::ClimateSwingModeMask *supported_swing_modes{}; const climate::ClimateSwingModeMask *supported_swing_modes{};
const std::vector<const char *> *supported_custom_fan_modes{}; const std::vector<std::string> *supported_custom_fan_modes{};
const climate::ClimatePresetMask *supported_presets{}; const climate::ClimatePresetMask *supported_presets{};
const std::vector<const char *> *supported_custom_presets{}; const std::vector<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};

View File

@@ -224,7 +224,7 @@ 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()) {

View File

@@ -237,7 +237,7 @@ extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-cons
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> { template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
public: public:
bool check(const Ts &...x) override { return global_api_server->is_connected(); } bool check(Ts... x) override { return global_api_server->is_connected(); }
}; };
} // namespace esphome::api } // namespace esphome::api

View File

@@ -133,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));

View File

@@ -7,6 +7,7 @@
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <type_traits>
#include <vector> #include <vector>
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
@@ -159,22 +160,6 @@ class ProtoVarInt {
} }
} }
} }
void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_;
if (val <= 0x7F) {
out.push_back(val);
return;
}
while (val) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
out.push_back(temp | 0x80);
} else {
out.push_back(temp);
}
}
}
protected: protected:
uint64_t value_; uint64_t value_;
@@ -233,8 +218,86 @@ class ProtoWriteBuffer {
public: public:
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
void write(uint8_t value) { this->buffer_->push_back(value); } void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } // Single implementation that all overloads delegate to
void encode_varint(uint64_t value) {
auto buffer = this->buffer_;
size_t start = buffer->size();
// Fast paths for common cases (1-3 bytes)
if (value < (1ULL << 7)) {
// 1 byte - very common for field IDs and small lengths
buffer->resize(start + 1);
buffer->data()[start] = static_cast<uint8_t>(value);
return;
}
uint8_t *p;
if (value < (1ULL << 14)) {
// 2 bytes - common for medium field IDs and lengths
buffer->resize(start + 2);
p = buffer->data() + start;
p[0] = (value & 0x7F) | 0x80;
p[1] = (value >> 7) & 0x7F;
return;
}
if (value < (1ULL << 21)) {
// 3 bytes - rare
buffer->resize(start + 3);
p = buffer->data() + start;
p[0] = (value & 0x7F) | 0x80;
p[1] = ((value >> 7) & 0x7F) | 0x80;
p[2] = (value >> 14) & 0x7F;
return;
}
// Rare case: 4-10 byte values - calculate size from bit position
// Value is guaranteed >= (1ULL << 21), so CLZ is safe (non-zero)
uint32_t size;
#if defined(__GNUC__) || defined(__clang__)
// Use compiler intrinsic for efficient bit position lookup
size = (64 - __builtin_clzll(value) + 6) / 7;
#else
// Fallback for compilers without __builtin_clzll
if (value < (1ULL << 28)) {
size = 4;
} else if (value < (1ULL << 35)) {
size = 5;
} else if (value < (1ULL << 42)) {
size = 6;
} else if (value < (1ULL << 49)) {
size = 7;
} else if (value < (1ULL << 56)) {
size = 8;
} else if (value < (1ULL << 63)) {
size = 9;
} else {
size = 10;
}
#endif
buffer->resize(start + size);
p = buffer->data() + start;
size_t bytes = 0;
while (value) {
uint8_t temp = value & 0x7F;
value >>= 7;
p[bytes++] = value ? temp | 0x80 : temp;
}
}
// Common case: uint32_t values (field IDs, lengths, most integers)
void encode_varint(uint32_t value) { this->encode_varint(static_cast<uint64_t>(value)); }
// size_t overload (only enabled if size_t is distinct from uint32_t and uint64_t)
template<typename T>
void encode_varint(T value) requires(std::is_same_v<T, size_t> && !std::is_same_v<size_t, uint32_t> &&
!std::is_same_v<size_t, uint64_t>) {
this->encode_varint(static_cast<uint64_t>(value));
}
// Rare case: ProtoVarInt wrapper
void encode_varint(ProtoVarInt value) { this->encode_varint(value.as_uint64()); }
/** /**
* Encode a field key (tag/wire type combination). * Encode a field key (tag/wire type combination).
* *
@@ -249,14 +312,14 @@ class ProtoWriteBuffer {
*/ */
void encode_field_raw(uint32_t field_id, uint32_t type) { void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
this->encode_varint_raw(val); this->encode_varint(val);
} }
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force) if (len == 0 && !force)
return; return;
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len); this->encode_varint(len);
// Using resize + memcpy instead of insert provides significant performance improvement: // Using resize + memcpy instead of insert provides significant performance improvement:
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
@@ -278,13 +341,13 @@ class ProtoWriteBuffer {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint32 this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
this->encode_varint_raw(value); this->encode_varint(value);
} }
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64 this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value)); this->encode_varint(value);
} }
void encode_bool(uint32_t field_id, bool value, bool force = false) { void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force) if (!value && !force)

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

@@ -100,6 +100,7 @@ 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 constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
} // namespace bedjet } // namespace bedjet
} // namespace esphome } // namespace esphome

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

@@ -50,13 +50,21 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
// 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

@@ -548,6 +548,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

@@ -141,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_;
@@ -153,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);
} }
@@ -166,7 +166,7 @@ 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_;

View File

@@ -89,7 +89,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> { template<typename... Ts> class ResetEnergyAction : public Action<Ts...>, public Parented<BL0906> {
public: public:
void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); } void play(Ts... x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
}; };
} // namespace bl0906 } // namespace bl0906

View File

@@ -123,9 +123,9 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
this->has_simple_value_ = true; this->has_simple_value_ = true;
} }
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
this->var_ = std::make_tuple(x...); this->var_ = std::make_tuple(x...);
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...); auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
@@ -229,7 +229,7 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
public: public:
BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientPasskeyReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
uint32_t passkey; uint32_t passkey;
if (has_simple_value_) { if (has_simple_value_) {
passkey = this->value_.simple; passkey = this->value_.simple;
@@ -266,7 +266,7 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
public: public:
BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientNumericComparisonReplyAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) { if (has_simple_value_) {
@@ -299,7 +299,7 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
public: public:
BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; } BLEClientRemoveBondAction(BLEClient *ble_client) { parent_ = ble_client; }
void play(const Ts &...x) override { void play(Ts... x) override {
esp_bd_addr_t remote_bda; esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t)); memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
esp_ble_remove_bond_device(remote_bda); esp_ble_remove_bond_device(remote_bda);
@@ -334,9 +334,9 @@ template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, pu
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
// it makes no sense to have multiple instances of this running at the same time. // it makes no sense to have multiple instances of this running at the same time.
// this would occur only if the same automation was re-triggered while still // this would occur only if the same automation was re-triggered while still
// running. So just cancel the second chain if this is detected. // running. So just cancel the second chain if this is detected.
@@ -379,9 +379,9 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
} }
// not used since we override play_complex_ // not used since we override play_complex_
void play(const Ts &...x) override {} void play(Ts... x) override {}
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
if (this->node_state == espbt::ClientState::IDLE) { if (this->node_state == espbt::ClientState::IDLE) {
this->play_next_(x...); this->play_next_(x...);

View File

@@ -77,9 +77,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }

View File

@@ -79,9 +79,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
} }
} else { } else {
this->node_state = espbt::ClientState::ESTABLISHED; this->node_state = espbt::ClientState::ESTABLISHED;
// For non-notify characteristics, trigger an immediate read after service discovery
// to avoid peripherals disconnecting due to inactivity
this->update();
} }
break; break;
} }

View File

@@ -84,6 +84,11 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema) return _BUTTON_SCHEMA.extend(schema)
# Remove before 2025.11.0
BUTTON_SCHEMA = button_schema(Button)
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config): async def setup_button_core_(var, config):
await setup_entity(var, config, "button") await setup_entity(var, config, "button")

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class PressAction : public Action<Ts...> {
public: public:
explicit PressAction(Button *button) : button_(button) {} explicit PressAction(Button *button) : button_(button) {}
void play(const Ts &...x) override { this->button_->press(); } void play(Ts... x) override { this->button_->press(); }
protected: protected:
Button *button_; Button *button_;

View File

@@ -129,7 +129,7 @@ template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public P
this->remote_transmission_request_ = remote_transmission_request; this->remote_transmission_request_ = remote_transmission_request;
} }
void play(const Ts &...x) override { void play(Ts... x) override {
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_; auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id = auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_; this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;

View File

@@ -270,6 +270,11 @@ def climate_schema(
return _CLIMATE_SCHEMA.extend(schema) return _CLIMATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
CLIMATE_SCHEMA = climate_schema(Climate)
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config): async def setup_climate_core_(var, config):
await setup_entity(var, config, "climate") await setup_entity(var, config, "climate")

View File

@@ -22,7 +22,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, custom_preset) TEMPLATABLE_VALUE(std::string, custom_preset)
TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode) TEMPLATABLE_VALUE(ClimateSwingMode, swing_mode)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->climate_->make_call(); auto call = this->climate_->make_call();
call.set_mode(this->mode_.optional_value(x...)); call.set_mode(this->mode_.optional_value(x...));
call.set_target_temperature(this->target_temperature_.optional_value(x...)); call.set_target_temperature(this->target_temperature_.optional_value(x...));

View File

@@ -50,21 +50,21 @@ void ClimateCall::perform() {
const LogString *mode_s = climate_mode_to_string(*this->mode_); const LogString *mode_s = climate_mode_to_string(*this->mode_);
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s)); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(mode_s));
} }
if (this->custom_fan_mode_ != nullptr) { if (this->custom_fan_mode_.has_value()) {
this->fan_mode_.reset(); this->fan_mode_.reset();
ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str());
} }
if (this->fan_mode_.has_value()) { if (this->fan_mode_.has_value()) {
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_);
ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s)); ESP_LOGD(TAG, " Fan: %s", LOG_STR_ARG(fan_mode_s));
} }
if (this->custom_preset_ != nullptr) { if (this->custom_preset_.has_value()) {
this->preset_.reset(); this->preset_.reset();
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str());
} }
if (this->preset_.has_value()) { if (this->preset_.has_value()) {
this->custom_preset_ = nullptr; this->custom_preset_.reset();
const LogString *preset_s = climate_preset_to_string(*this->preset_); const LogString *preset_s = climate_preset_to_string(*this->preset_);
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s)); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(preset_s));
} }
@@ -96,10 +96,11 @@ void ClimateCall::validate_() {
this->mode_.reset(); this->mode_.reset();
} }
} }
if (this->custom_fan_mode_ != nullptr) { if (this->custom_fan_mode_.has_value()) {
if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { auto custom_fan_mode = *this->custom_fan_mode_;
ESP_LOGW(TAG, " Fan Mode %s not supported", this->custom_fan_mode_); if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
this->custom_fan_mode_ = nullptr; ESP_LOGW(TAG, " Fan Mode %s not supported", custom_fan_mode.c_str());
this->custom_fan_mode_.reset();
} }
} else if (this->fan_mode_.has_value()) { } else if (this->fan_mode_.has_value()) {
auto fan_mode = *this->fan_mode_; auto fan_mode = *this->fan_mode_;
@@ -108,10 +109,11 @@ void ClimateCall::validate_() {
this->fan_mode_.reset(); this->fan_mode_.reset();
} }
} }
if (this->custom_preset_ != nullptr) { if (this->custom_preset_.has_value()) {
if (!traits.supports_custom_preset(this->custom_preset_)) { auto custom_preset = *this->custom_preset_;
ESP_LOGW(TAG, " Preset %s not supported", this->custom_preset_); if (!traits.supports_custom_preset(custom_preset)) {
this->custom_preset_ = nullptr; ESP_LOGW(TAG, " Preset %s not supported", custom_preset.c_str());
this->custom_preset_.reset();
} }
} else if (this->preset_.has_value()) { } else if (this->preset_.has_value()) {
auto preset = *this->preset_; auto preset = *this->preset_;
@@ -184,29 +186,26 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) {
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
// Check if it's a standard enum mode first
for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) { for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
if (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
return *this;
} }
} }
// Find the matching pointer from parent climate device if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { this->custom_fan_mode_ = fan_mode;
this->custom_fan_mode_ = mode_ptr;
this->fan_mode_.reset(); this->fan_mode_.reset();
return *this; } else {
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); }
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
if (fan_mode.has_value()) { if (fan_mode.has_value()) {
this->set_fan_mode(fan_mode.value()); this->set_fan_mode(fan_mode.value());
@@ -216,29 +215,26 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_ = nullptr; this->custom_preset_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const char *custom_preset) { ClimateCall &ClimateCall::set_preset(const std::string &preset) {
// Check if it's a standard enum preset first
for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) { for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
if (str_equals_case_insensitive(custom_preset, preset_entry.str)) { if (str_equals_case_insensitive(preset, preset_entry.str)) {
return this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
return *this;
} }
} }
// Find the matching pointer from parent climate device if (this->parent_->get_traits().supports_custom_preset(preset)) {
if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { this->custom_preset_ = preset;
this->custom_preset_ = preset_ptr;
this->preset_.reset(); this->preset_.reset();
return *this; } else {
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
} }
ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset);
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); }
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
if (preset.has_value()) { if (preset.has_value()) {
this->set_preset(preset.value()); this->set_preset(preset.value());
@@ -291,6 +287,8 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_;
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
this->target_temperature_high_ = target_temperature_high; this->target_temperature_high_ = target_temperature_high;
@@ -319,13 +317,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
this->fan_mode_ = fan_mode; this->fan_mode_ = fan_mode;
this->custom_fan_mode_ = nullptr; this->custom_fan_mode_.reset();
return *this; return *this;
} }
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
this->preset_ = preset; this->preset_ = preset;
this->custom_preset_ = nullptr; this->custom_preset_.reset();
return *this; return *this;
} }
@@ -384,13 +382,13 @@ void Climate::save_state_() {
state.uses_custom_fan_mode = false; state.uses_custom_fan_mode = false;
state.fan_mode = this->fan_mode.value(); state.fan_mode = this->fan_mode.value();
} }
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
state.uses_custom_fan_mode = true; state.uses_custom_fan_mode = true;
const auto &supported = traits.get_supported_custom_fan_modes(); const auto &supported = traits.get_supported_custom_fan_modes();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const char *mode : supported) { for (const auto &mode : supported) {
if (strcmp(mode, this->custom_fan_mode_) == 0) { if (mode == custom_fan_mode) {
state.custom_fan_mode = i; state.custom_fan_mode = i;
break; break;
} }
@@ -401,13 +399,13 @@ void Climate::save_state_() {
state.uses_custom_preset = false; state.uses_custom_preset = false;
state.preset = this->preset.value(); state.preset = this->preset.value();
} }
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
state.uses_custom_preset = true; state.uses_custom_preset = true;
const auto &supported = traits.get_supported_custom_presets(); const auto &supported = traits.get_supported_custom_presets();
// std::vector maintains insertion order // std::vector maintains insertion order
size_t i = 0; size_t i = 0;
for (const char *preset : supported) { for (const auto &preset : supported) {
if (strcmp(preset, this->custom_preset_) == 0) { if (preset == custom_preset) {
state.custom_preset = i; state.custom_preset = i;
break; break;
} }
@@ -432,14 +430,14 @@ void Climate::publish_state() {
if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) {
ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); ESP_LOGD(TAG, " Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value())));
} }
if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) {
ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode_); ESP_LOGD(TAG, " Custom Fan Mode: %s", this->custom_fan_mode.value().c_str());
} }
if (traits.get_supports_presets() && this->preset.has_value()) { if (traits.get_supports_presets() && this->preset.has_value()) {
ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); ESP_LOGD(TAG, " Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value())));
} }
if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) {
ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset.value().c_str());
} }
if (traits.get_supports_swing_modes()) { if (traits.get_supports_swing_modes()) {
ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); ESP_LOGD(TAG, " Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode)));
@@ -529,7 +527,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
call.fan_mode_.reset(); call.fan_mode_.reset();
call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
call.set_fan_mode(this->fan_mode); call.set_fan_mode(this->fan_mode);
@@ -537,7 +535,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
call.preset_.reset(); call.preset_.reset();
call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
call.set_preset(this->preset); call.set_preset(this->preset);
@@ -564,20 +562,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
if (this->uses_custom_fan_mode) { if (this->uses_custom_fan_mode) {
if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
climate->fan_mode.reset(); climate->fan_mode.reset();
climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
} }
} else if (traits.supports_fan_mode(this->fan_mode)) { } else if (traits.supports_fan_mode(this->fan_mode)) {
climate->fan_mode = this->fan_mode; climate->fan_mode = this->fan_mode;
climate->clear_custom_fan_mode_(); climate->custom_fan_mode.reset();
} }
if (this->uses_custom_preset) { if (this->uses_custom_preset) {
if (this->custom_preset < traits.get_supported_custom_presets().size()) { if (this->custom_preset < traits.get_supported_custom_presets().size()) {
climate->preset.reset(); climate->preset.reset();
climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
} }
} else if (traits.supports_preset(this->preset)) { } else if (traits.supports_preset(this->preset)) {
climate->preset = this->preset; climate->preset = this->preset;
climate->clear_custom_preset_(); climate->custom_preset.reset();
} }
if (traits.supports_swing_mode(this->swing_mode)) { if (traits.supports_swing_mode(this->swing_mode)) {
climate->swing_mode = this->swing_mode; climate->swing_mode = this->swing_mode;
@@ -585,107 +583,28 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
climate->publish_state(); climate->publish_state();
} }
/** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) {
* bool is_changed = alt.has_value();
* Climate devices have mutually exclusive mode pairs: alt.reset();
* - fan_mode (enum) vs custom_fan_mode_ (const char*) if (is_changed || dst != src) {
* - preset (enum) vs custom_preset_ (const char*) dst = src;
* is_changed = true;
* Only one mode in each pair can be active at a time. This helper ensures setting a primary
* mode automatically clears its corresponding custom mode.
*
* Example state transitions:
* Before: custom_fan_mode_="Turbo", fan_mode=nullopt
* Call: set_fan_mode_(CLIMATE_FAN_HIGH)
* After: custom_fan_mode_=nullptr, fan_mode=CLIMATE_FAN_HIGH
*
* @param primary The primary mode optional (fan_mode or preset)
* @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_)
* @param value The new primary mode value to set
* @return true if state changed, false if already set to this value
*/
template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) {
// Clear the custom mode (mutual exclusion)
bool changed = custom_ptr != nullptr;
custom_ptr = nullptr;
// Set the primary mode
if (changed || !primary.has_value() || primary.value() != value) {
primary = value;
return true;
} }
return false; return is_changed;
}
/** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion.
*
* This helper ensures setting a custom mode automatically clears its corresponding primary mode.
* It also validates that the custom mode exists in the device's supported modes (lifetime safety).
*
* Example state transitions:
* Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr
* Call: set_custom_fan_mode_("Turbo")
* After: fan_mode=nullopt, custom_fan_mode_="Turbo" (pointer from traits)
*
* Lifetime Safety:
* - found_ptr must come from traits.find_custom_*_mode_()
* - Only pointers found in traits are stored, ensuring they remain valid
* - Prevents dangling pointers from temporary strings
*
* @param custom_ptr Reference to the custom mode pointer to set
* @param primary The primary mode optional to clear
* @param found_ptr The validated pointer from traits (nullptr if not found)
* @param has_custom Whether a custom mode is currently active
* @return true if state changed, false otherwise
*/
template<typename T>
bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) {
if (found_ptr != nullptr) {
// Clear the primary mode (mutual exclusion)
bool changed = primary.has_value();
primary.reset();
// Set the custom mode (pointer is validated by caller from traits)
if (changed || custom_ptr != found_ptr) {
custom_ptr = found_ptr;
return true;
}
return false;
}
// Mode not found in supported modes, clear it if currently set
if (has_custom) {
custom_ptr = nullptr;
return true;
}
return false;
} }
bool Climate::set_fan_mode_(ClimateFanMode mode) { bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); return set_alternative(this->fan_mode, this->custom_fan_mode, mode);
} }
bool Climate::set_custom_fan_mode_(const char *mode) { bool Climate::set_custom_fan_mode_(const std::string &mode) {
auto traits = this->get_traits(); return set_alternative(this->custom_fan_mode, this->fan_mode, mode);
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
this->has_custom_fan_mode());
} }
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); }
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } bool Climate::set_custom_preset_(const std::string &preset) {
return set_alternative(this->custom_preset, this->preset, preset);
bool Climate::set_custom_preset_(const char *preset) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
this->has_custom_preset());
}
void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; }
const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) {
return this->get_traits().find_custom_fan_mode_(custom_fan_mode);
}
const char *Climate::find_custom_preset_(const char *custom_preset) {
return this->get_traits().find_custom_preset_(custom_preset);
} }
void Climate::dump_traits_(const char *tag) { void Climate::dump_traits_(const char *tag) {
@@ -737,8 +656,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_fan_modes().empty()) { if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:"); ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const char *s : traits.get_supported_custom_fan_modes()) for (const std::string &s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s); ESP_LOGCONFIG(tag, " - %s", s.c_str());
} }
if (!traits.get_supported_presets().empty()) { if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:"); ESP_LOGCONFIG(tag, " Supported presets:");
@@ -747,8 +666,8 @@ void Climate::dump_traits_(const char *tag) {
} }
if (!traits.get_supported_custom_presets().empty()) { if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:"); ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const char *s : traits.get_supported_custom_presets()) for (const std::string &s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s); ESP_LOGCONFIG(tag, " - %s", s.c_str());
} }
if (!traits.get_supported_swing_modes().empty()) { if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:"); ESP_LOGCONFIG(tag, " Supported swing modes:");

View File

@@ -77,8 +77,6 @@ class ClimateCall {
ClimateCall &set_fan_mode(const std::string &fan_mode); ClimateCall &set_fan_mode(const std::string &fan_mode);
/// Set the fan mode of the climate device based on a string. /// Set the fan mode of the climate device based on a string.
ClimateCall &set_fan_mode(optional<std::string> fan_mode); ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); ClimateCall &set_swing_mode(ClimateSwingMode swing_mode);
/// Set the swing mode of the climate device. /// Set the swing mode of the climate device.
@@ -93,8 +91,6 @@ class ClimateCall {
ClimateCall &set_preset(const std::string &preset); ClimateCall &set_preset(const std::string &preset);
/// Set the preset of the climate device based on a string. /// Set the preset of the climate device based on a string.
ClimateCall &set_preset(optional<std::string> preset); ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
void perform(); void perform();
@@ -107,10 +103,8 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const; const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const; const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const; const optional<ClimatePreset> &get_preset() const;
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } const optional<std::string> &get_custom_fan_mode() const;
const char *get_custom_preset() const { return this->custom_preset_; } const optional<std::string> &get_custom_preset() const;
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
protected: protected:
void validate_(); void validate_();
@@ -124,10 +118,8 @@ class ClimateCall {
optional<ClimateFanMode> fan_mode_; optional<ClimateFanMode> fan_mode_;
optional<ClimateSwingMode> swing_mode_; optional<ClimateSwingMode> swing_mode_;
optional<ClimatePreset> preset_; optional<ClimatePreset> preset_;
optional<std::string> custom_fan_mode_;
private: optional<std::string> custom_preset_;
const char *custom_fan_mode_{nullptr};
const char *custom_preset_{nullptr};
}; };
/// Struct used to save the state of the climate device in restore memory. /// Struct used to save the state of the climate device in restore memory.
@@ -220,12 +212,6 @@ class Climate : public EntityBase {
void set_visual_min_humidity_override(float visual_min_humidity_override); void set_visual_min_humidity_override(float visual_min_humidity_override);
void set_visual_max_humidity_override(float visual_max_humidity_override); void set_visual_max_humidity_override(float visual_max_humidity_override);
/// Check if a custom fan mode is currently active.
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
/// Check if a custom preset is currently active.
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
/// The current temperature of the climate device, as reported from the integration. /// The current temperature of the climate device, as reported from the integration.
float current_temperature{NAN}; float current_temperature{NAN};
@@ -252,6 +238,12 @@ class Climate : public EntityBase {
/// The active preset of the climate device. /// The active preset of the climate device.
optional<ClimatePreset> preset; optional<ClimatePreset> preset;
/// The active custom fan mode of the climate device.
optional<std::string> custom_fan_mode;
/// The active custom preset mode of the climate device.
optional<std::string> custom_preset;
/// The active mode of the climate device. /// The active mode of the climate device.
ClimateMode mode{CLIMATE_MODE_OFF}; ClimateMode mode{CLIMATE_MODE_OFF};
@@ -261,37 +253,20 @@ class Climate : public EntityBase {
/// The active swing mode of the climate device. /// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
/// Get the active custom fan mode (read-only access).
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
/// Get the active custom preset (read-only access).
const char *get_custom_preset() const { return this->custom_preset_; }
protected: protected:
friend ClimateCall; friend ClimateCall;
friend struct ClimateDeviceRestoreState;
/// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed.
bool set_fan_mode_(ClimateFanMode mode); bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const char *mode); bool set_custom_fan_mode_(const std::string &mode);
/// Clear custom fan mode.
void clear_custom_fan_mode_();
/// Set preset. Reset custom preset. Return true if preset has been changed. /// Set preset. Reset custom preset. Return true if preset has been changed.
bool set_preset_(ClimatePreset preset); bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed. /// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const char *preset); bool set_custom_preset_(const std::string &preset);
/// Clear custom preset.
void clear_custom_preset_();
/// Find and return the matching custom fan mode pointer from traits, or nullptr if not found.
const char *find_custom_fan_mode_(const char *custom_fan_mode);
/// Find and return the matching custom preset pointer from traits, or nullptr if not found.
const char *find_custom_preset_(const char *custom_preset);
/** Get the default traits of this climate device. /** Get the default traits of this climate device.
* *
@@ -328,21 +303,6 @@ class Climate : public EntityBase {
optional<float> visual_current_temperature_step_override_{}; optional<float> visual_current_temperature_step_override_{};
optional<float> visual_min_humidity_override_{}; optional<float> visual_min_humidity_override_{};
optional<float> visual_max_humidity_override_{}; optional<float> visual_max_humidity_override_{};
private:
/** The active custom fan mode (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_fan_modes_ or nullptr.
* Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify.
*/
const char *custom_fan_mode_{nullptr};
/** The active custom preset (private - enforces use of safe setters).
*
* Points to an entry in traits.supported_custom_presets_ or nullptr.
* Use get_custom_preset() to read, set_custom_preset_() to modify.
*/
const char *custom_preset_{nullptr};
}; };
} // namespace climate } // namespace climate

View File

@@ -1,6 +1,5 @@
#pragma once #pragma once
#include <cstring>
#include <vector> #include <vector>
#include "climate_mode.h" #include "climate_mode.h"
#include "esphome/core/finite_set_mask.h" #include "esphome/core/finite_set_mask.h"
@@ -19,25 +18,16 @@ using ClimateSwingModeMask =
FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>; FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>; using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
// Lightweight linear search for small vectors (1-20 items) of const char* pointers // Lightweight linear search for small vectors (1-20 items)
// Avoids std::find template overhead // Avoids std::find template overhead
inline bool vector_contains(const std::vector<const char *> &vec, const char *value) { template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
for (const char *item : vec) { for (const auto &item : vec) {
if (strcmp(item, value) == 0) if (item == value)
return true; return true;
} }
return false; return false;
} }
// Find and return matching pointer from vector, or nullptr if not found
inline const char *vector_find(const std::vector<const char *> &vec, const char *value) {
for (const char *item : vec) {
if (strcmp(item, value) == 0)
return item;
}
return nullptr;
}
/** This class contains all static data for climate devices. /** This class contains all static data for climate devices.
* *
* All climate devices must support these features: * All climate devices must support these features:
@@ -65,11 +55,7 @@ inline const char *vector_find(const std::vector<const char *> &vec, const char
* - temperature step - the step with which to increase/decrease target temperature. * - temperature step - the step with which to increase/decrease target temperature.
* This also affects with how many decimal places the temperature is shown * This also affects with how many decimal places the temperature is shown
*/ */
class Climate; // Forward declaration
class ClimateTraits { class ClimateTraits {
friend class Climate; // Allow Climate to access protected find methods
public: public:
/// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h)
uint32_t get_feature_flags() const { return this->feature_flags_; } uint32_t get_feature_flags() const { return this->feature_flags_; }
@@ -142,60 +128,46 @@ class ClimateTraits {
void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; } void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); } void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); } bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
bool get_supports_fan_modes() const { bool get_supports_fan_modes() const {
return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty(); return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
} }
const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; } const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) { void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
this->supported_custom_fan_modes_ = modes; this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
} }
void set_supported_custom_fan_modes(const std::vector<const char *> &modes) { void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) {
this->supported_custom_fan_modes_ = modes; this->supported_custom_fan_modes_ = modes;
} }
template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) { template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
this->supported_custom_fan_modes_.assign(modes, modes + N); this->supported_custom_fan_modes_.assign(modes, modes + N);
} }
const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete;
void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete;
const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
bool supports_custom_fan_mode(const char *custom_fan_mode) const {
return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
}
bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
return this->supports_custom_fan_mode(custom_fan_mode.c_str()); return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
} }
void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); } void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); } bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
bool get_supports_presets() const { return !this->supported_presets_.empty(); } bool get_supports_presets() const { return !this->supported_presets_.empty(); }
const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
void set_supported_custom_presets(std::initializer_list<const char *> presets) { void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
this->supported_custom_presets_ = presets; this->supported_custom_presets_ = std::move(supported_custom_presets);
} }
void set_supported_custom_presets(const std::vector<const char *> &presets) { void set_supported_custom_presets(std::initializer_list<std::string> presets) {
this->supported_custom_presets_ = presets; this->supported_custom_presets_ = presets;
} }
template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) { template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
this->supported_custom_presets_.assign(presets, presets + N); this->supported_custom_presets_.assign(presets, presets + N);
} }
const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
// Deleted overloads to catch incorrect std::string usage at compile time with clear error messages
void set_supported_custom_presets(const std::vector<std::string> &presets) = delete;
void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete;
const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
bool supports_custom_preset(const char *custom_preset) const {
return vector_contains(this->supported_custom_presets_, custom_preset);
}
bool supports_custom_preset(const std::string &custom_preset) const { bool supports_custom_preset(const std::string &custom_preset) const {
return this->supports_custom_preset(custom_preset.c_str()); return vector_contains(this->supported_custom_presets_, custom_preset);
} }
void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
@@ -255,18 +227,6 @@ class ClimateTraits {
} }
} }
/// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead
const char *find_custom_fan_mode_(const char *custom_fan_mode) const {
return vector_find(this->supported_custom_fan_modes_, custom_fan_mode);
}
/// Find and return the matching custom preset pointer from supported presets, or nullptr if not found
/// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead
const char *find_custom_preset_(const char *custom_preset) const {
return vector_find(this->supported_custom_presets_, custom_preset);
}
uint32_t feature_flags_{0}; uint32_t feature_flags_{0};
float visual_min_temperature_{10}; float visual_min_temperature_{10};
float visual_max_temperature_{30}; float visual_max_temperature_{30};
@@ -279,17 +239,8 @@ class ClimateTraits {
climate::ClimateFanModeMask supported_fan_modes_; climate::ClimateFanModeMask supported_fan_modes_;
climate::ClimateSwingModeMask supported_swing_modes_; climate::ClimateSwingModeMask supported_swing_modes_;
climate::ClimatePresetMask supported_presets_; climate::ClimatePresetMask supported_presets_;
std::vector<std::string> supported_custom_fan_modes_;
/** Custom mode storage using const char* pointers to eliminate std::string overhead. std::vector<std::string> supported_custom_presets_;
*
* Pointers must remain valid for the ClimateTraits lifetime. Safe patterns:
* - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"})
* - Static const data: static const char* MODE = "Eco";
*
* Climate class setters validate pointers are from these vectors before storing.
*/
std::vector<const char *> supported_custom_fan_modes_;
std::vector<const char *> supported_custom_presets_;
}; };
} // namespace climate } // namespace climate

View File

@@ -1,9 +1,10 @@
import logging import logging
from esphome import core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@@ -51,6 +52,26 @@ def climate_ir_with_receiver_schema(
) )
# Remove before 2025.11.0
def deprecated_schema_constant(config):
type: str = "unknown"
if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
type = str(id.type).split("::", maxsplit=1)[0]
_LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config): async def register_climate_ir(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await remote_base.register_transmittable(var, config) await remote_base.register_transmittable(var, config)

View File

@@ -30,7 +30,7 @@ template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...>
public: public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {} CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(const Ts &...x) override { this->cm1106_->calibrate_zero(400); } void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected: protected:
CM1106Component *cm1106_; CM1106Component *cm1106_;

View File

@@ -8,9 +8,7 @@ BYTE_ORDER_BIG = "big_endian"
CONF_COLOR_DEPTH = "color_depth" CONF_COLOR_DEPTH = "color_depth"
CONF_DRAW_ROUNDING = "draw_rounding" CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLED = "enabled"
CONF_ON_RECEIVE = "on_receive" CONF_ON_RECEIVE = "on_receive"
CONF_ON_STATE_CHANGE = "on_state_change" CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers" CONF_REQUEST_HEADERS = "request_headers"
CONF_ROWS = "rows"
CONF_USE_PSRAM = "use_psram" CONF_USE_PSRAM = "use_psram"

View File

@@ -12,7 +12,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode()); this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
}); });
@@ -20,7 +20,7 @@ void CopyFan::setup() {
this->oscillating = source_->oscillating; this->oscillating = source_->oscillating;
this->speed = source_->speed; this->speed = source_->speed;
this->direction = source_->direction; this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode()); this->preset_mode = source_->preset_mode;
this->publish_state(); this->publish_state();
} }
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
call2.set_speed(*call.get_speed()); call2.set_speed(*call.get_speed());
if (call.get_direction().has_value()) if (call.get_direction().has_value())
call2.set_direction(*call.get_direction()); call2.set_direction(*call.get_direction());
if (call.has_preset_mode()) if (!call.get_preset_mode().empty())
call2.set_preset_mode(call.get_preset_mode()); call2.set_preset_mode(call.get_preset_mode());
call2.perform(); call2.perform();
} }

View File

@@ -7,19 +7,19 @@ namespace copy {
static const char *const TAG = "copy.select"; static const char *const TAG = "copy.select";
void CopySelect::setup() { void CopySelect::setup() {
source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); });
traits.set_options(source_->traits.get_options()); traits.set_options(source_->traits.get_options());
if (source_->has_state()) if (source_->has_state())
this->publish_state(source_->active_index().value()); this->publish_state(source_->state);
} }
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
void CopySelect::control(size_t index) { void CopySelect::control(const std::string &value) {
auto call = source_->make_call(); auto call = source_->make_call();
call.set_index(index); call.set_option(value);
call.perform(); call.perform();
} }

View File

@@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component {
void dump_config() override; void dump_config() override;
protected: protected:
void control(size_t index) override; void control(const std::string &value) override;
select::Select *source_; select::Select *source_;
}; };

View File

@@ -151,6 +151,11 @@ def cover_schema(
return _COVER_SCHEMA.extend(schema) return _COVER_SCHEMA.extend(schema)
# Remove before 2025.11.0
COVER_SCHEMA = cover_schema(Cover)
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config): async def setup_cover_core_(var, config):
await setup_entity(var, config, "cover") await setup_entity(var, config, "cover")

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class OpenAction : public Action<Ts...> {
public: public:
explicit OpenAction(Cover *cover) : cover_(cover) {} explicit OpenAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_open().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_open().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -21,7 +21,7 @@ template<typename... Ts> class CloseAction : public Action<Ts...> {
public: public:
explicit CloseAction(Cover *cover) : cover_(cover) {} explicit CloseAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_close().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_close().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -31,7 +31,7 @@ template<typename... Ts> class StopAction : public Action<Ts...> {
public: public:
explicit StopAction(Cover *cover) : cover_(cover) {} explicit StopAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_stop().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_stop().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -41,7 +41,7 @@ template<typename... Ts> class ToggleAction : public Action<Ts...> {
public: public:
explicit ToggleAction(Cover *cover) : cover_(cover) {} explicit ToggleAction(Cover *cover) : cover_(cover) {}
void play(const Ts &...x) override { this->cover_->make_call().set_command_toggle().perform(); } void play(Ts... x) override { this->cover_->make_call().set_command_toggle().perform(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -55,7 +55,7 @@ template<typename... Ts> class ControlAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, position) TEMPLATABLE_VALUE(float, position)
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->cover_->make_call(); auto call = this->cover_->make_call();
if (this->stop_.has_value()) if (this->stop_.has_value())
call.set_stop(this->stop_.value(x...)); call.set_stop(this->stop_.value(x...));
@@ -77,7 +77,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, tilt) TEMPLATABLE_VALUE(float, tilt)
TEMPLATABLE_VALUE(CoverOperation, current_operation) TEMPLATABLE_VALUE(CoverOperation, current_operation)
void play(const Ts &...x) override { void play(Ts... x) override {
if (this->position_.has_value()) if (this->position_.has_value())
this->cover_->position = this->position_.value(x...); this->cover_->position = this->position_.value(x...);
if (this->tilt_.has_value()) if (this->tilt_.has_value())
@@ -94,7 +94,7 @@ template<typename... Ts> class CoverPublishAction : public Action<Ts...> {
template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
public: public:
CoverIsOpenCondition(Cover *cover) : cover_(cover) {} CoverIsOpenCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } bool check(Ts... x) override { return this->cover_->is_fully_open(); }
protected: protected:
Cover *cover_; Cover *cover_;
@@ -103,7 +103,7 @@ template<typename... Ts> class CoverIsOpenCondition : public Condition<Ts...> {
template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> { template<typename... Ts> class CoverIsClosedCondition : public Condition<Ts...> {
public: public:
CoverIsClosedCondition(Cover *cover) : cover_(cover) {} CoverIsClosedCondition(Cover *cover) : cover_(cover) {}
bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } bool check(Ts... x) override { return this->cover_->is_fully_closed(); }
protected: protected:
Cover *cover_; Cover *cover_;

View File

@@ -114,7 +114,7 @@ template<typename... Ts> class CS5460ARestartAction : public Action<Ts...> {
public: public:
CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {} CS5460ARestartAction(CS5460AComponent *cs5460a) : cs5460a_(cs5460a) {}
void play(const Ts &...x) override { cs5460a_->restart(); } void play(Ts... x) override { cs5460a_->restart(); }
protected: protected:
CS5460AComponent *cs5460a_; CS5460AComponent *cs5460a_;

View File

@@ -70,7 +70,7 @@ bool DallasTemperatureSensor::read_scratch_pad_() {
} }
void DallasTemperatureSensor::setup() { void DallasTemperatureSensor::setup() {
if (!this->check_address_or_index_()) if (!this->check_address_())
return; return;
if (!this->read_scratch_pad_()) if (!this->read_scratch_pad_())
return; return;

View File

@@ -101,7 +101,7 @@ template<typename... Ts> class DateSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, date) TEMPLATABLE_VALUE(ESPTime, date)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->date_.has_value()) { if (this->date_.has_value()) {

View File

@@ -124,7 +124,7 @@ template<typename... Ts> class DateTimeSetAction : public Action<Ts...>, public
public: public:
TEMPLATABLE_VALUE(ESPTime, datetime) TEMPLATABLE_VALUE(ESPTime, datetime)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->datetime_.has_value()) { if (this->datetime_.has_value()) {

View File

@@ -103,7 +103,7 @@ template<typename... Ts> class TimeSetAction : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(ESPTime, time) TEMPLATABLE_VALUE(ESPTime, time)
void play(const Ts &...x) override { void play(Ts... x) override {
auto call = this->parent_->make_call(); auto call = this->parent_->make_call();
if (this->time_.has_value()) { if (this->time_.has_value()) {

View File

@@ -8,7 +8,8 @@
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0] #define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
namespace esphome::debug { namespace esphome {
namespace debug {
static const char *const TAG = "debug"; static const char *const TAG = "debug";
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC; constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
@@ -280,18 +281,14 @@ void DebugComponent::get_device_info_(std::string &device_info) {
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE)); NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
<< UICR_PSELRESET_CONNECT_Pos;
ESP_LOGD( ESP_LOGD(
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s", TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)), YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
YESNO(n_reset_enabled)); YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
if (n_reset_enabled) { (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
uint8_t port = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PORT_Msk) >> UICR_PSELRESET_PORT_Pos; ((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
uint8_t pin = (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_PIN_Msk) >> UICR_PSELRESET_PIN_Pos; (UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
ESP_LOGD(TAG, "nRESET port P%u.%02u", port, pin);
}
#ifdef USE_BOOTLOADER_MCUBOOT #ifdef USE_BOOTLOADER_MCUBOOT
ESP_LOGD(TAG, "bootloader: mcuboot"); ESP_LOGD(TAG, "bootloader: mcuboot");
#else #else
@@ -325,22 +322,10 @@ void DebugComponent::get_device_info_(std::string &device_info) {
#endif #endif
} }
#endif #endif
auto uicr = [](volatile uint32_t *data, uint8_t size) {
std::string res;
char buf[sizeof(uint32_t) * 2 + 1];
for (size_t i = 0; i < size; i++) {
if (i > 0) {
res += ' ';
}
res += format_hex_pretty<uint32_t>(data[i], '\0', false);
}
return res;
};
ESP_LOGD(TAG, "NRFFW %s", uicr(NRF_UICR->NRFFW, 13).c_str());
ESP_LOGD(TAG, "NRFHW %s", uicr(NRF_UICR->NRFHW, 12).c_str());
} }
void DebugComponent::update_platform_() {} void DebugComponent::update_platform_() {}
} // namespace esphome::debug } // namespace debug
} // namespace esphome
#endif #endif

View File

@@ -148,7 +148,7 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
void set_time(time::RealTimeClock *time) { this->time_ = time; } void set_time(time::RealTimeClock *time) { this->time_ = time; }
#endif #endif
void play(const Ts &...x) override { void play(Ts... x) override {
if (this->sleep_duration_.has_value()) { if (this->sleep_duration_.has_value()) {
this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...)); this->deep_sleep_->set_sleep_duration(this->sleep_duration_.value(x...));
} }
@@ -207,12 +207,12 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> {
template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class PreventDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(const Ts &...x) override { this->parent_->prevent_deep_sleep(); } void play(Ts... x) override { this->parent_->prevent_deep_sleep(); }
}; };
template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> { template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, public Parented<DeepSleepComponent> {
public: public:
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); } void play(Ts... x) override { this->parent_->allow_deep_sleep(); }
}; };
} // namespace deep_sleep } // namespace deep_sleep

View File

@@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component {
this->mode = climate::CLIMATE_MODE_AUTO; this->mode = climate::CLIMATE_MODE_AUTO;
this->action = climate::CLIMATE_ACTION_COOLING; this->action = climate::CLIMATE_ACTION_COOLING;
this->fan_mode = climate::CLIMATE_FAN_HIGH; this->fan_mode = climate::CLIMATE_FAN_HIGH;
this->set_custom_preset_("My Preset"); this->custom_preset = {"My Preset"};
break; break;
case DemoClimateType::TYPE_3: case DemoClimateType::TYPE_3:
this->current_temperature = 21.5; this->current_temperature = 21.5;
this->target_temperature_low = 21.0; this->target_temperature_low = 21.0;
this->target_temperature_high = 22.5; this->target_temperature_high = 22.5;
this->mode = climate::CLIMATE_MODE_HEAT_COOL; this->mode = climate::CLIMATE_MODE_HEAT_COOL;
this->set_custom_fan_mode_("Auto Low"); this->custom_fan_mode = {"Auto Low"};
this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL;
this->set_preset_(climate::CLIMATE_PRESET_AWAY); this->preset = climate::CLIMATE_PRESET_AWAY;
break; break;
} }
this->publish_state(); this->publish_state();
@@ -58,19 +58,23 @@ class DemoClimate : public climate::Climate, public Component {
this->target_temperature_high = *call.get_target_temperature_high(); this->target_temperature_high = *call.get_target_temperature_high();
} }
if (call.get_fan_mode().has_value()) { if (call.get_fan_mode().has_value()) {
this->set_fan_mode_(*call.get_fan_mode()); this->fan_mode = *call.get_fan_mode();
this->custom_fan_mode.reset();
} }
if (call.get_swing_mode().has_value()) { if (call.get_swing_mode().has_value()) {
this->swing_mode = *call.get_swing_mode(); this->swing_mode = *call.get_swing_mode();
} }
if (call.has_custom_fan_mode()) { if (call.get_custom_fan_mode().has_value()) {
this->set_custom_fan_mode_(call.get_custom_fan_mode()); this->custom_fan_mode = *call.get_custom_fan_mode();
this->fan_mode.reset();
} }
if (call.get_preset().has_value()) { if (call.get_preset().has_value()) {
this->set_preset_(*call.get_preset()); this->preset = *call.get_preset();
this->custom_preset.reset();
} }
if (call.has_custom_preset()) { if (call.get_custom_preset().has_value()) {
this->set_custom_preset_(call.get_custom_preset()); this->custom_preset = *call.get_custom_preset();
this->preset.reset();
} }
this->publish_state(); this->publish_state();
} }

View File

@@ -77,7 +77,7 @@ class DFPlayer : public uart::UARTDevice, public Component {
class ACTION_CLASS : /* NOLINT */ \ class ACTION_CLASS : /* NOLINT */ \
public Action<Ts...>, \ public Action<Ts...>, \
public Parented<DFPlayer> { \ public Parented<DFPlayer> { \
void play(const Ts &...x) override { this->parent_->ACTION_METHOD(); } \ void play(Ts... x) override { this->parent_->ACTION_METHOD(); } \
}; };
DFPLAYER_SIMPLE_ACTION(NextAction, next) DFPLAYER_SIMPLE_ACTION(NextAction, next)
@@ -87,7 +87,7 @@ template<typename... Ts> class PlayMp3Action : public Action<Ts...>, public Pare
public: public:
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
void play(const Ts &...x) override { void play(Ts... x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
this->parent_->play_mp3(file); this->parent_->play_mp3(file);
} }
@@ -98,7 +98,7 @@ template<typename... Ts> class PlayFileAction : public Action<Ts...>, public Par
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override { void play(Ts... x) override {
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
if (loop) { if (loop) {
@@ -115,7 +115,7 @@ template<typename... Ts> class PlayFolderAction : public Action<Ts...>, public P
TEMPLATABLE_VALUE(uint16_t, file) TEMPLATABLE_VALUE(uint16_t, file)
TEMPLATABLE_VALUE(bool, loop) TEMPLATABLE_VALUE(bool, loop)
void play(const Ts &...x) override { void play(Ts... x) override {
auto folder = this->folder_.value(x...); auto folder = this->folder_.value(x...);
auto file = this->file_.value(x...); auto file = this->file_.value(x...);
auto loop = this->loop_.value(x...); auto loop = this->loop_.value(x...);
@@ -131,7 +131,7 @@ template<typename... Ts> class SetDeviceAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(Device, device) TEMPLATABLE_VALUE(Device, device)
void play(const Ts &...x) override { void play(Ts... x) override {
auto device = this->device_.value(x...); auto device = this->device_.value(x...);
this->parent_->set_device(device); this->parent_->set_device(device);
} }
@@ -141,7 +141,7 @@ template<typename... Ts> class SetVolumeAction : public Action<Ts...>, public Pa
public: public:
TEMPLATABLE_VALUE(uint8_t, volume) TEMPLATABLE_VALUE(uint8_t, volume)
void play(const Ts &...x) override { void play(Ts... x) override {
auto volume = this->volume_.value(x...); auto volume = this->volume_.value(x...);
this->parent_->set_volume(volume); this->parent_->set_volume(volume);
} }
@@ -151,7 +151,7 @@ template<typename... Ts> class SetEqAction : public Action<Ts...>, public Parent
public: public:
TEMPLATABLE_VALUE(EqPreset, eq) TEMPLATABLE_VALUE(EqPreset, eq)
void play(const Ts &...x) override { void play(Ts... x) override {
auto eq = this->eq_.value(x...); auto eq = this->eq_.value(x...);
this->parent_->set_eq(eq); this->parent_->set_eq(eq);
} }
@@ -168,7 +168,7 @@ DFPLAYER_SIMPLE_ACTION(VolumeDownAction, volume_down)
template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> { template<typename... Ts> class DFPlayerIsPlayingCondition : public Condition<Ts...>, public Parented<DFPlayer> {
public: public:
bool check(const Ts &...x) override { return this->parent_->is_playing(); } bool check(Ts... x) override { return this->parent_->is_playing(); }
}; };
class DFPlayerFinishedPlaybackTrigger : public Trigger<> { class DFPlayerFinishedPlaybackTrigger : public Trigger<> {

View File

@@ -11,7 +11,7 @@ namespace dfrobot_sen0395 {
template<typename... Ts> template<typename... Ts>
class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> { class DfrobotSen0395ResetAction : public Action<Ts...>, public Parented<DfrobotSen0395Component> {
public: public:
void play(const Ts &...x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); } void play(Ts... x) { this->parent_->enqueue(make_unique<ResetSystemCommand>()); }
}; };
template<typename... Ts> template<typename... Ts>
@@ -33,7 +33,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
TEMPLATABLE_VALUE(float, det_min4) TEMPLATABLE_VALUE(float, det_min4)
TEMPLATABLE_VALUE(float, det_max4) TEMPLATABLE_VALUE(float, det_max4)
void play(const Ts &...x) { void play(Ts... x) {
this->parent_->enqueue(make_unique<PowerCommand>(0)); this->parent_->enqueue(make_unique<PowerCommand>(0));
if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) { if (this->factory_reset_.has_value() && this->factory_reset_.value(x...) == true) {
this->parent_->enqueue(make_unique<FactoryResetCommand>()); this->parent_->enqueue(make_unique<FactoryResetCommand>());

View File

@@ -176,117 +176,7 @@ class Display;
class DisplayPage; class DisplayPage;
class DisplayOnPageChangeTrigger; class DisplayOnPageChangeTrigger;
/** Optimized display writer that uses function pointers for stateless lambdas. using display_writer_t = std::function<void(Display &)>;
*
* Similar to TemplatableValue but specialized for display writer callbacks.
* Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer).
*
* Supports both:
* - Stateless lambdas (from YAML) → function pointer (4 bytes)
* - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated)
*
* @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay)
*/
template<typename T> class DisplayWriter {
public:
DisplayWriter() : type_(NONE) {}
// For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
: type_(STATELESS_LAMBDA) {
this->stateless_f_ = f; // Implicit conversion to function pointer
}
// For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
// This handles backwards compatibility with external components
template<typename F>
DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
this->f_ = new std::function<void(T &)>(std::move(f));
}
// Copy constructor
DisplayWriter(const DisplayWriter &other) : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = new std::function<void(T &)>(*other.f_);
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
}
// Move constructor
DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) {
if (type_ == LAMBDA) {
this->f_ = other.f_;
other.f_ = nullptr;
} else if (type_ == STATELESS_LAMBDA) {
this->stateless_f_ = other.stateless_f_;
}
other.type_ = NONE;
}
// Assignment operators
DisplayWriter &operator=(const DisplayWriter &other) {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(other);
}
return *this;
}
DisplayWriter &operator=(DisplayWriter &&other) noexcept {
if (this != &other) {
this->~DisplayWriter();
new (this) DisplayWriter(std::move(other));
}
return *this;
}
~DisplayWriter() {
if (type_ == LAMBDA) {
delete this->f_;
}
// STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty)
}
bool has_value() const { return this->type_ != NONE; }
void call(T &display) const {
switch (this->type_) {
case STATELESS_LAMBDA:
this->stateless_f_(display); // Direct function pointer call
break;
case LAMBDA:
(*this->f_)(display); // std::function call
break;
case NONE:
default:
break;
}
}
// Operator() for convenience
void operator()(T &display) const { this->call(display); }
// Operator* for backwards compatibility with (*writer_)(*this) pattern
DisplayWriter &operator*() { return *this; }
const DisplayWriter &operator*() const { return *this; }
protected:
enum : uint8_t {
NONE,
LAMBDA,
STATELESS_LAMBDA,
} type_;
union {
std::function<void(T &)> *f_;
void (*stateless_f_)(T &);
};
};
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
using display_writer_t = DisplayWriter<Display>;
#define LOG_DISPLAY(prefix, type, obj) \ #define LOG_DISPLAY(prefix, type, obj) \
if ((obj) != nullptr) { \ if ((obj) != nullptr) { \
@@ -320,7 +210,7 @@ class Display : public PollingComponent {
/// Fill the entire screen with the given color. /// Fill the entire screen with the given color.
virtual void fill(Color color); virtual void fill(Color color);
/// Clear the entire screen by filling it with OFF pixels. /// Clear the entire screen by filling it with OFF pixels.
virtual void clear(); void clear();
/// Get the calculated width of the display in pixels with rotation applied. /// Get the calculated width of the display in pixels with rotation applied.
virtual int get_width() { return this->get_width_internal(); } virtual int get_width() { return this->get_width_internal(); }
@@ -788,7 +678,7 @@ class Display : public PollingComponent {
void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3); void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES}; DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
display_writer_t writer_{}; optional<display_writer_t> writer_{};
DisplayPage *page_{nullptr}; DisplayPage *page_{nullptr};
DisplayPage *previous_page_{nullptr}; DisplayPage *previous_page_{nullptr};
std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_; std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
@@ -819,7 +709,7 @@ template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> {
public: public:
TEMPLATABLE_VALUE(DisplayPage *, page) TEMPLATABLE_VALUE(DisplayPage *, page)
void play(const Ts &...x) override { void play(Ts... x) override {
auto *page = this->page_.value(x...); auto *page = this->page_.value(x...);
if (page != nullptr) { if (page != nullptr) {
page->show(); page->show();
@@ -831,7 +721,7 @@ template<typename... Ts> class DisplayPageShowNextAction : public Action<Ts...>
public: public:
DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowNextAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_next_page(); } void play(Ts... x) override { this->buffer_->show_next_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -840,7 +730,7 @@ template<typename... Ts> class DisplayPageShowPrevAction : public Action<Ts...>
public: public:
DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {} DisplayPageShowPrevAction(Display *buffer) : buffer_(buffer) {}
void play(const Ts &...x) override { this->buffer_->show_prev_page(); } void play(Ts... x) override { this->buffer_->show_prev_page(); }
Display *buffer_; Display *buffer_;
}; };
@@ -850,7 +740,7 @@ template<typename... Ts> class DisplayIsDisplayingPageCondition : public Conditi
DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {} DisplayIsDisplayingPageCondition(Display *parent) : parent_(parent) {}
void set_page(DisplayPage *page) { this->page_ = page; } void set_page(DisplayPage *page) { this->page_ = page; }
bool check(const Ts &...x) override { return this->parent_->get_active_page() == this->page_; } bool check(Ts... x) override { return this->parent_->get_active_page() == this->page_; }
protected: protected:
Display *parent_; Display *parent_;

View File

@@ -10,7 +10,7 @@ template<typename... Ts> class UpAction : public Action<Ts...> {
public: public:
explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit UpAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->up(); } void play(Ts... x) override { this->menu_->up(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -20,7 +20,7 @@ template<typename... Ts> class DownAction : public Action<Ts...> {
public: public:
explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit DownAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->down(); } void play(Ts... x) override { this->menu_->down(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -30,7 +30,7 @@ template<typename... Ts> class LeftAction : public Action<Ts...> {
public: public:
explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit LeftAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->left(); } void play(Ts... x) override { this->menu_->left(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -40,7 +40,7 @@ template<typename... Ts> class RightAction : public Action<Ts...> {
public: public:
explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit RightAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->right(); } void play(Ts... x) override { this->menu_->right(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -50,7 +50,7 @@ template<typename... Ts> class EnterAction : public Action<Ts...> {
public: public:
explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit EnterAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->enter(); } void play(Ts... x) override { this->menu_->enter(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -60,7 +60,7 @@ template<typename... Ts> class ShowAction : public Action<Ts...> {
public: public:
explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->show(); } void play(Ts... x) override { this->menu_->show(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -70,7 +70,7 @@ template<typename... Ts> class HideAction : public Action<Ts...> {
public: public:
explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit HideAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->hide(); } void play(Ts... x) override { this->menu_->hide(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -80,7 +80,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
public: public:
explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {} explicit ShowMainAction(DisplayMenuComponent *menu) : menu_(menu) {}
void play(const Ts &...x) override { this->menu_->show_main(); } void play(Ts... x) override { this->menu_->show_main(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;
@@ -88,7 +88,7 @@ template<typename... Ts> class ShowMainAction : public Action<Ts...> {
template<typename... Ts> class IsActiveCondition : public Condition<Ts...> { template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
public: public:
explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {} explicit IsActiveCondition(DisplayMenuComponent *menu) : menu_(menu) {}
bool check(const Ts &...x) override { return this->menu_->is_active(); } bool check(Ts... x) override { return this->menu_->is_active(); }
protected: protected:
DisplayMenuComponent *menu_; DisplayMenuComponent *menu_;

View File

@@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
result = this->value_getter_.value()(this); result = this->value_getter_.value()(this);
} else { } else {
if (this->select_var_ != nullptr) { if (this->select_var_ != nullptr) {
result = this->select_var_->current_option(); result = this->select_var_->state;
} }
} }

View File

@@ -59,12 +59,12 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice {
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(const Ts &...x) override { this->parent_->write_time(); } void play(Ts... x) override { this->parent_->write_time(); }
}; };
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> { template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<DS1307Component> {
public: public:
void play(const Ts &...x) override { this->parent_->read_time(); } void play(Ts... x) override { this->parent_->read_time(); }
}; };
} // namespace ds1307 } // namespace ds1307
} // namespace esphome } // namespace esphome

View File

@@ -51,15 +51,15 @@ class DutyTimeSensor : public sensor::Sensor, public PollingComponent {
template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {}; template<typename... Ts> class BaseAction : public Action<Ts...>, public Parented<DutyTimeSensor> {};
template<typename... Ts> class StartAction : public BaseAction<Ts...> { template<typename... Ts> class StartAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->start(); } void play(Ts... x) override { this->parent_->start(); }
}; };
template<typename... Ts> class StopAction : public BaseAction<Ts...> { template<typename... Ts> class StopAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->stop(); } void play(Ts... x) override { this->parent_->stop(); }
}; };
template<typename... Ts> class ResetAction : public BaseAction<Ts...> { template<typename... Ts> class ResetAction : public BaseAction<Ts...> {
void play(const Ts &...x) override { this->parent_->reset(); } void play(Ts... x) override { this->parent_->reset(); }
}; };
template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> { template<typename... Ts> class RunningCondition : public Condition<Ts...>, public Parented<DutyTimeSensor> {
@@ -67,7 +67,7 @@ template<typename... Ts> class RunningCondition : public Condition<Ts...>, publi
explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {} explicit RunningCondition(DutyTimeSensor *parent, bool state) : Parented(parent), state_(state) {}
protected: protected:
bool check(const Ts &...x) override { return this->parent_->is_running() == this->state_; } bool check(Ts... x) override { return this->parent_->is_running() == this->state_; }
bool state_; bool state_;
}; };

View File

@@ -3,8 +3,6 @@
#include "e131_addressable_light_effect.h" #include "e131_addressable_light_effect.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include <algorithm>
namespace esphome { namespace esphome {
namespace e131 { namespace e131 {
@@ -78,14 +76,14 @@ void E131Component::loop() {
} }
void E131Component::add_effect(E131AddressableLightEffect *light_effect) { void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) { if (light_effects_.count(light_effect)) {
return; return;
} }
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe()); light_effect->get_last_universe());
light_effects_.push_back(light_effect); light_effects_.insert(light_effect);
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
join_(universe); join_(universe);
@@ -93,17 +91,14 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
} }
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect); if (!light_effects_.count(light_effect)) {
if (it == light_effects_.end()) {
return; return;
} }
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe()); light_effect->get_last_universe());
// Swap with last element and pop for O(1) removal (order doesn't matter) light_effects_.erase(light_effect);
*it = light_effects_.back();
light_effects_.pop_back();
for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) { for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
leave_(universe); leave_(universe);

View File

@@ -7,6 +7,7 @@
#include <cinttypes> #include <cinttypes>
#include <map> #include <map>
#include <memory> #include <memory>
#include <set>
#include <vector> #include <vector>
namespace esphome { namespace esphome {
@@ -46,8 +47,9 @@ class E131Component : public esphome::Component {
E131ListenMethod listen_method_{E131_MULTICAST}; E131ListenMethod listen_method_{E131_MULTICAST};
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::vector<E131AddressableLightEffect *> light_effects_; std::set<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_; std::map<int, int> universe_consumers_;
std::map<int, E131Packet> universe_packets_;
}; };
} // namespace e131 } // namespace e131

View File

@@ -1,35 +1,21 @@
import importlib
import pkgutil
from esphome import core, pins from esphome import core, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import display, spi from esphome.components import display, spi
from esphome.components.mipi import flatten_sequence, map_sequence
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BUSY_PIN, CONF_BUSY_PIN,
CONF_CS_PIN,
CONF_DATA_RATE,
CONF_DC_PIN, CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_ENABLE_PIN,
CONF_HEIGHT,
CONF_ID, CONF_ID,
CONF_INIT_SEQUENCE,
CONF_LAMBDA, CONF_LAMBDA,
CONF_MODEL, CONF_MODEL,
CONF_PAGES,
CONF_RESET_DURATION, CONF_RESET_DURATION,
CONF_RESET_PIN, CONF_RESET_PIN,
CONF_WIDTH,
) )
from . import models
AUTO_LOAD = ["split_buffer"] AUTO_LOAD = ["split_buffer"]
DEPENDENCIES = ["spi"] DEPENDENCIES = ["spi"]
CONF_INIT_SEQUENCE_ID = "init_sequence_id"
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi") epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
EPaperBase = epaper_spi_ns.class_( EPaperBase = epaper_spi_ns.class_(
"EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
@@ -38,78 +24,29 @@ EPaperBase = epaper_spi_ns.class_(
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase) EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6) EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
MODELS = {
# Import all models dynamically from the models package "7.3in-spectra-e6": EPaper7p3InSpectraE6,
for module_info in pkgutil.iter_modules(models.__path__): }
importlib.import_module(f".models.{module_info.name}", package=__package__)
MODELS = models.EpaperModel.models
DIMENSION_SCHEMA = cv.Schema(
{
cv.Required(CONF_WIDTH): cv.int_,
cv.Required(CONF_HEIGHT): cv.int_,
}
)
def model_schema(config): CONFIG_SCHEMA = cv.All(
model = MODELS[config[CONF_MODEL]] display.FULL_DISPLAY_SCHEMA.extend(
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
return (
display.FULL_DISPLAY_SCHEMA.extend(
spi.spi_device_schema(
cs_pin_required=False,
default_mode="MODE0",
default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000),
)
)
.extend(
{
model.option(pin): pins.gpio_output_pin_schema
for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN)
}
)
.extend(
{
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema,
cv.GenerateID(): cv.declare_id(class_name),
cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8),
cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA,
model.option(CONF_ENABLE_PIN): cv.ensure_list(
pins.gpio_output_pin_schema
),
model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list(
map_sequence
),
model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
)
def customise_schema(config):
"""
Create a customised config schema for a specific model and validate the configuration.
:param config: The configuration dictionary to validate
:return: The validated configuration dictionary
:raises cv.Invalid: If the configuration is invalid
"""
config = cv.Schema(
{ {
cv.Required(CONF_MODEL): cv.one_of(*MODELS, upper=True), cv.GenerateID(): cv.declare_id(EPaperBase),
}, cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
extra=cv.ALLOW_EXTRA, cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
)(config) cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
return model_schema(config)(config) cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
cv.Optional(CONF_RESET_DURATION): cv.All(
cv.positive_time_period_milliseconds,
CONFIG_SCHEMA = customise_schema cv.Range(max=core.TimePeriod(milliseconds=500)),
),
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema()),
cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
)
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema( FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
"epaper_spi", require_miso=False, require_mosi=True "epaper_spi", require_miso=False, require_mosi=True
@@ -119,23 +56,8 @@ FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
async def to_code(config): async def to_code(config):
model = MODELS[config[CONF_MODEL]] model = MODELS[config[CONF_MODEL]]
init_sequence = config.get(CONF_INIT_SEQUENCE) rhs = model.new()
if init_sequence is None: var = cg.Pvariable(config[CONF_ID], rhs, model)
init_sequence = model.get_init_sequence(config)
init_sequence = flatten_sequence(init_sequence)
init_sequence_length = len(init_sequence)
init_sequence_id = cg.static_const_array(
config[CONF_INIT_SEQUENCE_ID], init_sequence
)
width, height = model.get_dimensions(config)
var = cg.new_Pvariable(
config[CONF_ID],
model.name,
width,
height,
init_sequence_id,
init_sequence_length,
)
await display.register_display(var, config) await display.register_display(var, config)
await spi.register_spi_device(var, config) await spi.register_spi_device(var, config)

View File

@@ -8,20 +8,33 @@ namespace esphome::epaper_spi {
static const char *const TAG = "epaper_spi"; static const char *const TAG = "epaper_spi";
static constexpr const char *const EPAPER_STATE_STRINGS[] = { static const LogString *epaper_state_to_string(EPaperState state) {
"IDLE", "UPDATE", "RESET", "RESET_END", switch (state) {
case EPaperState::IDLE:
"SHOULD_WAIT", "INITIALISE", "TRANSFER_DATA", "POWER_ON", "REFRESH_SCREEN", "POWER_OFF", "DEEP_SLEEP", return LOG_STR("IDLE");
}; case EPaperState::UPDATE:
return LOG_STR("UPDATE");
const char *EPaperBase::epaper_state_to_string_() { case EPaperState::RESET:
if (auto idx = static_cast<unsigned>(this->state_); idx < std::size(EPAPER_STATE_STRINGS)) return LOG_STR("RESET");
return EPAPER_STATE_STRINGS[idx]; case EPaperState::INITIALISE:
return "Unknown"; return LOG_STR("INITIALISE");
case EPaperState::TRANSFER_DATA:
return LOG_STR("TRANSFER_DATA");
case EPaperState::POWER_ON:
return LOG_STR("POWER_ON");
case EPaperState::REFRESH_SCREEN:
return LOG_STR("REFRESH_SCREEN");
case EPaperState::POWER_OFF:
return LOG_STR("POWER_OFF");
case EPaperState::DEEP_SLEEP:
return LOG_STR("DEEP_SLEEP");
default:
return LOG_STR("UNKNOWN");
}
} }
void EPaperBase::setup() { void EPaperBase::setup() {
if (!this->init_buffer_(this->buffer_length_)) { if (!this->init_buffer_(this->get_buffer_length())) {
this->mark_failed("Failed to initialise buffer"); this->mark_failed("Failed to initialise buffer");
return; return;
} }
@@ -37,7 +50,7 @@ bool EPaperBase::init_buffer_(size_t buffer_length) {
return true; return true;
} }
void EPaperBase::setup_pins_() const { void EPaperBase::setup_pins_() {
this->dc_pin_->setup(); // OUTPUT this->dc_pin_->setup(); // OUTPUT
this->dc_pin_->digital_write(false); this->dc_pin_->digital_write(false);
@@ -68,7 +81,11 @@ void EPaperBase::data(uint8_t value) {
// write a command followed by zero or more bytes of data. // write a command followed by zero or more bytes of data.
// The command is the first byte, length is the length of data only in the second byte, followed by the data. // The command is the first byte, length is the length of data only in the second byte, followed by the data.
// [COMMAND, LENGTH, DATA...] // [COMMAND, LENGTH, DATA...]
void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) { void EPaperBase::cmd_data(const uint8_t *data) {
const uint8_t command = data[0];
const uint8_t length = data[1];
const uint8_t *ptr = data + 2;
ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length, ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
format_hex_pretty(ptr, length, '.', false).c_str()); format_hex_pretty(ptr, length, '.', false).c_str());
@@ -82,146 +99,91 @@ void EPaperBase::cmd_data(uint8_t command, const uint8_t *ptr, size_t length) {
this->disable(); this->disable();
} }
bool EPaperBase::is_idle_() const { bool EPaperBase::is_idle_() {
if (this->busy_pin_ == nullptr) { if (this->busy_pin_ == nullptr) {
return true; return true;
} }
return !this->busy_pin_->digital_read(); return this->busy_pin_->digital_read();
} }
bool EPaperBase::reset_() const { void EPaperBase::reset() {
if (this->reset_pin_ != nullptr) { if (this->reset_pin_ != nullptr) {
if (this->state_ == EPaperState::RESET) { this->reset_pin_->digital_write(false);
this->reset_pin_->digital_write(false); this->disable_loop();
return false; this->set_timeout(this->reset_duration_, [this] {
} this->reset_pin_->digital_write(true);
this->reset_pin_->digital_write(true); this->set_timeout(20, [this] { this->enable_loop(); });
});
} }
return true;
} }
void EPaperBase::update() { void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) { if (!this->state_queue_.empty()) {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_()); ESP_LOGE(TAG, "Display update already in progress - %s",
LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
return; return;
} }
this->set_state_(EPaperState::RESET);
this->state_queue_.push(EPaperState::UPDATE);
this->state_queue_.push(EPaperState::RESET);
this->state_queue_.push(EPaperState::INITIALISE);
this->state_queue_.push(EPaperState::TRANSFER_DATA);
this->state_queue_.push(EPaperState::POWER_ON);
this->state_queue_.push(EPaperState::REFRESH_SCREEN);
this->state_queue_.push(EPaperState::POWER_OFF);
this->state_queue_.push(EPaperState::DEEP_SLEEP);
this->state_queue_.push(EPaperState::IDLE);
this->enable_loop(); this->enable_loop();
} }
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (should_wait) {
this->waiting_for_idle_start_ = millis();
this->waiting_for_idle_last_print_ = this->waiting_for_idle_start_;
}
#endif
this->waiting_for_idle_ = should_wait;
}
/**
* Called during the loop task.
* First defer for any pending delays, then check if we are waiting for the display to become idle.
* If not waiting for idle, process the state machine.
*/
void EPaperBase::loop() { void EPaperBase::loop() {
auto now = millis();
if (this->delay_until_ != 0) {
// using modulus arithmetic to handle wrap-around
int diff = now - this->delay_until_;
if (diff < 0) {
return;
}
this->delay_until_ = 0;
}
if (this->waiting_for_idle_) { if (this->waiting_for_idle_) {
if (this->is_idle_()) { if (this->is_idle_()) {
this->waiting_for_idle_ = false; this->waiting_for_idle_ = false;
ESP_LOGV(TAG, "Screen now idle after %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
} else { } else {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
if (now - this->waiting_for_idle_last_print_ >= 1000) { ESP_LOGV(TAG, "Waiting for idle");
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_()); this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
this->waiting_for_idle_last_print_ = millis();
} }
#endif
return; return;
} }
} }
this->process_state_();
}
/** auto state = this->state_queue_.front();
* Process the state machine.
* Typical state sequence: switch (state) {
* IDLE -> RESET -> RESET_END -> UPDATE -> INITIALISE -> TRANSFER_DATA -> POWER_ON -> REFRESH_SCREEN -> POWER_OFF ->
* DEEP_SLEEP -> IDLE
*
* Should a subclassed class need to override this, the method will need to be made virtual.
*/
void EPaperBase::process_state_() {
ESP_LOGV(TAG, "Process state entered in state %s", epaper_state_to_string_());
switch (this->state_) {
default:
ESP_LOGD(TAG, "Display is in unhandled state %s", epaper_state_to_string_());
this->disable_loop();
break;
case EPaperState::IDLE: case EPaperState::IDLE:
this->disable_loop(); this->disable_loop();
break; break;
case EPaperState::RESET:
case EPaperState::RESET_END:
if (this->reset_()) {
this->set_state_(EPaperState::UPDATE);
} else {
this->set_state_(EPaperState::RESET_END);
}
break;
case EPaperState::UPDATE: case EPaperState::UPDATE:
this->do_update_(); // Calls ESPHome (current page) lambda this->do_update_(); // Calls ESPHome (current page) lambda
this->set_state_(EPaperState::INITIALISE); break;
case EPaperState::RESET:
this->reset();
break; break;
case EPaperState::INITIALISE: case EPaperState::INITIALISE:
this->initialise_(); this->initialise_();
this->set_state_(EPaperState::TRANSFER_DATA);
break; break;
case EPaperState::TRANSFER_DATA: case EPaperState::TRANSFER_DATA:
if (!this->transfer_data()) { if (!this->transfer_data()) {
return; // Not done yet, come back next loop return; // Not done yet, come back next loop
} }
this->set_state_(EPaperState::POWER_ON);
break; break;
case EPaperState::POWER_ON: case EPaperState::POWER_ON:
this->power_on(); this->power_on();
this->set_state_(EPaperState::REFRESH_SCREEN);
break; break;
case EPaperState::REFRESH_SCREEN: case EPaperState::REFRESH_SCREEN:
this->refresh_screen(); this->refresh_screen();
this->set_state_(EPaperState::POWER_OFF);
break; break;
case EPaperState::POWER_OFF: case EPaperState::POWER_OFF:
this->power_off(); this->power_off();
this->set_state_(EPaperState::DEEP_SLEEP);
break; break;
case EPaperState::DEEP_SLEEP: case EPaperState::DEEP_SLEEP:
this->deep_sleep(); this->deep_sleep();
this->set_state_(EPaperState::IDLE);
break; break;
} }
} this->state_queue_.pop();
void EPaperBase::set_state_(EPaperState state, uint16_t delay) {
ESP_LOGV(TAG, "Exit state %s", this->epaper_state_to_string_());
this->state_ = state;
this->wait_for_idle_(state > EPaperState::SHOULD_WAIT);
if (delay != 0) {
this->delay_until_ = millis() + delay;
} else {
this->delay_until_ = 0;
}
ESP_LOGV(TAG, "Enter state %s, delay %u, wait_for_idle=%s", this->epaper_state_to_string_(), delay,
TRUEFALSE(this->waiting_for_idle_));
} }
void EPaperBase::start_command_() { void EPaperBase::start_command_() {
@@ -241,39 +203,25 @@ void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
void EPaperBase::initialise_() { void EPaperBase::initialise_() {
size_t index = 0; size_t index = 0;
const auto &sequence = this->init_sequence_;
auto *sequence = this->init_sequence_; const size_t sequence_size = this->init_sequence_length_;
auto length = this->init_sequence_length_; while (index != sequence_size) {
while (index != length) { if (sequence_size - index < 2) {
if (length - index < 2) {
this->mark_failed("Malformed init sequence"); this->mark_failed("Malformed init sequence");
return; return;
} }
const uint8_t cmd = sequence[index++]; const auto *ptr = sequence + index;
if (const uint8_t x = sequence[index++]; x == DELAY_FLAG) { const uint8_t length = ptr[1];
ESP_LOGV(TAG, "Delay %dms", cmd); if (sequence_size - index < length + 2) {
delay(cmd); this->mark_failed("Malformed init sequence");
} else { return;
const uint8_t num_args = x & 0x7F;
if (length - index < num_args) {
ESP_LOGE(TAG, "Malformed init sequence, cmd = %X, num_args = %u", cmd, num_args);
this->mark_failed();
return;
}
ESP_LOGV(TAG, "Command %02X, length %d", cmd, num_args);
this->cmd_data(cmd, sequence + index, num_args);
index += num_args;
} }
}
}
void EPaperBase::dump_config() { this->cmd_data(ptr);
LOG_DISPLAY("", "E-Paper SPI", this); index += length + 2;
ESP_LOGCONFIG(TAG, " Model: %s", this->name_); }
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_); this->power_on();
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
} }
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -8,48 +8,36 @@
#include <queue> #include <queue>
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
using namespace display;
enum class EPaperState : uint8_t { enum class EPaperState : uint8_t {
IDLE, // not doing anything IDLE,
UPDATE, // update the buffer UPDATE,
RESET, // drive reset low (active) RESET,
RESET_END, // drive reset high (inactive) INITIALISE,
TRANSFER_DATA,
SHOULD_WAIT, // states higher than this should wait for the display to be not busy POWER_ON,
INITIALISE, // send the init sequence REFRESH_SCREEN,
TRANSFER_DATA, // transfer data to the display POWER_OFF,
POWER_ON, // power on the display DEEP_SLEEP,
REFRESH_SCREEN, // send refresh command
POWER_OFF, // power off the display
DEEP_SLEEP, // deep sleep the display
}; };
static constexpr uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run static const uint8_t MAX_TRANSFER_TIME = 10; // Transfer in 10ms blocks to allow the loop to run
static constexpr uint8_t DELAY_FLAG = 0xFF;
class EPaperBase : public DisplayBuffer, class EPaperBase : public display::DisplayBuffer,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
spi::DATA_RATE_2MHZ> { spi::DATA_RATE_2MHZ> {
public: public:
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY) : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
: name_(name),
width_(width),
height_(height),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; } void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override; float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; } void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; } void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void dump_config() override;
void command(uint8_t value); void command(uint8_t value);
void data(uint8_t value); void data(uint8_t value);
void cmd_data(uint8_t command, const uint8_t *ptr, size_t length); void cmd_data(const uint8_t *data);
void update() override; void update() override;
void loop() override; void loop() override;
@@ -58,84 +46,48 @@ class EPaperBase : public DisplayBuffer,
void on_safe_shutdown() override; void on_safe_shutdown() override;
DisplayType get_display_type() override { return this->display_type_; };
protected: protected:
int get_height_internal() override { return this->height_; }; bool is_idle_();
int get_width_internal() override { return this->width_; }; void setup_pins_();
void process_state_(); virtual void reset();
const char *epaper_state_to_string_();
bool is_idle_() const;
void setup_pins_() const;
bool reset_() const;
void initialise_(); void initialise_();
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length); bool init_buffer_(size_t buffer_length);
virtual int get_width_controller() { return this->get_width_internal(); }; virtual int get_width_controller() { return this->get_width_internal(); };
virtual void deep_sleep() = 0;
/**
* Methods that must be implemented by concrete classes to control the display
*/
/** /**
* Send data to the device via SPI * Send data to the device via SPI
* @return true if done, false if it should be called next loop * @return true if done, false if should be called next loop
*/ */
virtual bool transfer_data() = 0; virtual bool transfer_data() = 0;
/**
* Refresh the screen after data transfer
*/
virtual void refresh_screen() = 0; virtual void refresh_screen() = 0;
/**
* Power the display on
*/
virtual void power_on() = 0; virtual void power_on() = 0;
/**
* Power the display off
*/
virtual void power_off() = 0; virtual void power_off() = 0;
virtual uint32_t get_buffer_length() = 0;
/**
* Place the display into deep sleep
*/
virtual void deep_sleep() = 0;
void set_state_(EPaperState state, uint16_t delay = 0);
void start_command_(); void start_command_();
void end_command_(); void end_command_();
void start_data_(); void start_data_();
void end_data_(); void end_data_();
// properties initialised in the constructor const size_t init_sequence_length_{0};
const char *name_;
uint16_t width_;
uint16_t height_;
const uint8_t *init_sequence_;
size_t init_sequence_length_;
DisplayType display_type_;
size_t buffer_length_{}; size_t current_data_index_{0};
size_t current_data_index_{0}; // used by data transfer to track progress
uint32_t reset_duration_{200}; uint32_t reset_duration_{200};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t transfer_start_time_{};
uint32_t waiting_for_idle_last_print_{0}; uint32_t waiting_for_idle_last_print_{0};
uint32_t waiting_for_idle_start_{0};
#endif
GPIOPin *dc_pin_{}; GPIOPin *dc_pin_;
GPIOPin *busy_pin_{}; GPIOPin *busy_pin_{nullptr};
GPIOPin *reset_pin_{}; GPIOPin *reset_pin_{nullptr};
const uint8_t *init_sequence_{nullptr};
bool waiting_for_idle_{false}; bool waiting_for_idle_{false};
uint32_t delay_until_{0};
split_buffer::SplitBuffer buffer_; split_buffer::SplitBuffer buffer_;
EPaperState state_{EPaperState::IDLE}; std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -0,0 +1,42 @@
#include "epaper_spi_model_7p3in_spectra_e6.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
void EPaper7p3InSpectraE6::power_on() {
ESP_LOGI(TAG, "Power on");
this->command(0x04);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::power_off() {
ESP_LOGI(TAG, "Power off");
this->command(0x02);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::refresh_screen() {
ESP_LOGI(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
this->waiting_for_idle_ = true;
}
void EPaper7p3InSpectraE6::deep_sleep() {
ESP_LOGI(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
}
void EPaper7p3InSpectraE6::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG, " Model: 7.3in Spectra E6");
LOG_PIN(" Reset Pin: ", this->reset_pin_);
LOG_PIN(" DC Pin: ", this->dc_pin_);
LOG_PIN(" Busy Pin: ", this->busy_pin_);
LOG_UPDATE_INTERVAL(this);
}
} // namespace esphome::epaper_spi

View File

@@ -0,0 +1,45 @@
#pragma once
#include "epaper_spi_spectra_e6.h"
namespace esphome::epaper_spi {
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
static constexpr const uint16_t WIDTH = 800;
static constexpr const uint16_t HEIGHT = 480;
// clang-format off
// Command, data length, data
static constexpr uint8_t INIT_SEQUENCE[] = {
0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
0x01, 1, 0x3F,
0x00, 2, 0x5F, 0x69,
0x03, 4, 0x00, 0x54, 0x00, 0x44,
0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
0x30, 1, 0x03,
0x50, 1, 0x3F,
0x60, 2, 0x02, 0x00,
0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
0x84, 1, 0x01,
0xE3, 1, 0x2F,
};
// clang-format on
public:
EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
void dump_config() override;
protected:
int get_width_internal() override { return WIDTH; };
int get_height_internal() override { return HEIGHT; };
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,166 +1,135 @@
#include "epaper_spi_spectra_e6.h" #include "epaper_spi_spectra_e6.h"
#include <algorithm>
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome::epaper_spi { namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.6c"; static constexpr const char *const TAG = "epaper_spi.6c";
static constexpr size_t MAX_TRANSFER_SIZE = 128;
static constexpr unsigned char GRAY_THRESHOLD = 50;
enum E6Color { static inline uint8_t color_to_hex(Color color) {
BLACK, if (color.red > 127) {
WHITE, if (color.green > 170) {
YELLOW, if (color.blue > 127) {
RED, return 0x1; // White
SKIP_1, } else {
BLUE, return 0x2; // Yellow
GREEN, }
CYAN, } else {
SKIP_2, return 0x3; // Red (or Magenta)
}; }
} else {
static uint8_t color_to_hex(Color color) { if (color.green > 127) {
// --- Step 1: Check for Grayscale (Black or White) --- if (color.blue > 127) {
// We define "grayscale" as a color where the min and max components return 0x5; // Cyan -> Blue
// are close to each other. } else {
unsigned char max_rgb = std::max({color.r, color.g, color.b}); return 0x6; // Green
unsigned char min_rgb = std::min({color.r, color.g, color.b}); }
} else {
if ((max_rgb - min_rgb) < GRAY_THRESHOLD) { if (color.blue > 127) {
// It's a shade of gray. Map to BLACK or WHITE. return 0x5; // Blue
// We split the luminance at the halfway point (382 = (255*3)/2) } else {
if ((static_cast<int>(color.r) + color.g + color.b) > 382) { return 0x0; // Black
return WHITE; }
} }
return BLACK;
} }
// --- Step 2: Check for Primary/Secondary Colors ---
// If it's not gray, it's a color. We check which components are
// "on" (over 128) vs "off". This divides the RGB cube into 8 corners.
bool r_on = (color.r > 128);
bool g_on = (color.g > 128);
bool b_on = (color.b > 128);
if (r_on && g_on && !b_on) {
return YELLOW;
}
if (r_on && !g_on && !b_on) {
return RED;
}
if (!r_on && g_on && !b_on) {
return GREEN;
}
if (!r_on && !g_on && b_on) {
return BLUE;
}
// Handle "impure" colors (Cyan, Magenta)
if (!r_on && g_on && b_on) {
// Cyan (G+B) -> Closest is Green or Blue. Pick Green.
return GREEN;
}
if (r_on && !g_on) {
// Magenta (R+B) -> Closest is Red or Blue. Pick Red.
return RED;
}
// Handle the remaining corners (White-ish, Black-ish)
if (r_on) {
// All high (but not gray) -> White
return WHITE;
}
// !r_on && !g_on && !b_on
// All low (but not gray) -> Black
return BLACK;
}
void EPaperSpectraE6::power_on() {
ESP_LOGD(TAG, "Power on");
this->command(0x04);
}
void EPaperSpectraE6::power_off() {
ESP_LOGD(TAG, "Power off");
this->command(0x02);
this->data(0x00);
}
void EPaperSpectraE6::refresh_screen() {
ESP_LOGD(TAG, "Refresh");
this->command(0x12);
this->data(0x00);
}
void EPaperSpectraE6::deep_sleep() {
ESP_LOGD(TAG, "Deep sleep");
this->command(0x07);
this->data(0xA5);
} }
void EPaperSpectraE6::fill(Color color) { void EPaperSpectraE6::fill(Color color) {
auto pixel_color = color_to_hex(color); uint8_t pixel_color;
if (color.is_on()) {
pixel_color = color_to_hex(color);
} else {
pixel_color = 0x1;
}
// We store 2 pixels per byte // We store 8 bitset<3> in 3 bytes
this->buffer_.fill(pixel_color + (pixel_color << 4)); // | byte 1 | byte 2 | byte 3 |
// |aaabbbaa|abbbaaab|bbaaabbb|
uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
const size_t buffer_length = this->get_buffer_length();
for (size_t i = 0; i < buffer_length; i += 3) {
this->buffer_[i + 0] = byte_1;
this->buffer_[i + 1] = byte_2;
this->buffer_[i + 2] = byte_3;
}
} }
void EPaperSpectraE6::clear() { uint32_t EPaperSpectraE6::get_buffer_length() {
// clear buffer to white, just like real paper. // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
this->fill(COLOR_ON); return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
} }
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) { void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0) if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
return; return;
auto pixel_bits = color_to_hex(color); uint8_t pixel_bits = color_to_hex(color);
uint32_t pixel_position = x + y * this->get_width_controller(); uint32_t pixel_position = x + y * this->get_width_controller();
uint32_t byte_position = pixel_position / 2; uint32_t first_bit_position = pixel_position * 3;
auto original = this->buffer_[byte_position]; uint32_t byte_position = first_bit_position / 8u;
if ((pixel_position & 1) != 0) { uint32_t byte_subposition = first_bit_position % 8u;
this->buffer_[byte_position] = (original & 0xF0) | pixel_bits;
if (byte_subposition <= 5) {
this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
(pixel_bits << (5 - byte_subposition));
} else { } else {
this->buffer_[byte_position] = (original & 0x0F) | (pixel_bits << 4); this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
(pixel_bits >> (byte_subposition - 5));
this->buffer_[byte_position + 1] =
(this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
(pixel_bits << (13 - byte_subposition));
} }
} }
bool HOT EPaperSpectraE6::transfer_data() { bool HOT EPaperSpectraE6::transfer_data() {
const uint32_t start_time = App.get_loop_component_start_time(); const uint32_t start_time = App.get_loop_component_start_time();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) { if (this->current_data_index_ == 0) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE ESP_LOGV(TAG, "Sending data");
this->transfer_start_time_ = millis();
#endif
ESP_LOGV(TAG, "Start sending data at %ums", (unsigned) millis());
this->command(0x10); this->command(0x10);
} }
size_t buf_idx = 0; uint8_t bytes_to_send[4]{0};
uint8_t bytes_to_send[MAX_TRANSFER_SIZE]; const size_t buffer_length = this->get_buffer_length();
while (this->current_data_index_ != buffer_length) { for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
bytes_to_send[buf_idx++] = this->buffer_[this->current_data_index_++]; const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
// 8 pixels are stored in 3 bytes
// |aaabbbaa|abbbaaab|bbaaabbb|
// | byte 1 | byte 2 | byte 3 |
bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
if (buf_idx == sizeof bytes_to_send) { this->start_data_();
this->start_data_(); this->write_array(bytes_to_send, sizeof(bytes_to_send));
this->write_array(bytes_to_send, buf_idx); this->end_data_();
this->end_data_();
ESP_LOGV(TAG, "Wrote %d bytes at %ums", buf_idx, (unsigned) millis());
buf_idx = 0;
if (millis() - start_time > MAX_TRANSFER_TIME) { if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop // Let the main loop run and come back next loop
return false; this->current_data_index_ = i + 3;
} return false;
} }
} }
// Finished the entire dataset // Finished the entire dataset
if (buf_idx != 0) {
this->start_data_();
this->write_array(bytes_to_send, buf_idx);
this->end_data_();
}
this->current_data_index_ = 0; this->current_data_index_ = 0;
ESP_LOGV(TAG, "Sent data in %" PRIu32 " ms", millis() - this->transfer_start_time_);
return true; return true;
} }
void EPaperSpectraE6::reset() {
if (this->reset_pin_ != nullptr) {
this->disable_loop();
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] {
this->reset_pin_->digital_write(false);
delay(2);
this->reset_pin_->digital_write(true);
this->set_timeout(20, [this] { this->enable_loop(); });
});
}
}
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -6,23 +6,18 @@ namespace esphome::epaper_spi {
class EPaperSpectraE6 : public EPaperBase { class EPaperSpectraE6 : public EPaperBase {
public: public:
EPaperSpectraE6(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence, EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
size_t init_sequence_length) : EPaperBase(init_sequence, init_sequence_length) {}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = width * height / 2; // 2 pixels per byte
}
display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
void fill(Color color) override; void fill(Color color) override;
void clear() override;
protected: protected:
void refresh_screen() override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void draw_absolute_pixel_internal(int x, int y, Color color) override; void draw_absolute_pixel_internal(int x, int y, Color color) override;
uint32_t get_buffer_length() override;
bool transfer_data() override; bool transfer_data() override;
void reset() override;
}; };
} // namespace esphome::epaper_spi } // namespace esphome::epaper_spi

View File

@@ -1,65 +0,0 @@
from typing import Any, Self
import esphome.config_validation as cv
from esphome.const import CONF_DIMENSIONS, CONF_HEIGHT, CONF_WIDTH
class EpaperModel:
models: dict[str, Self] = {}
def __init__(
self,
name: str,
class_name: str,
initsequence=None,
**defaults,
):
name = name.upper()
self.name = name
self.class_name = class_name
self.initsequence = initsequence
self.defaults = defaults
EpaperModel.models[name] = self
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
def get_init_sequence(self, config: dict):
return self.initsequence
def option(self, name, fallback=cv.UNDEFINED) -> cv.Optional | cv.Required:
if fallback is None and self.get_default(name, None) is None:
return cv.Required(name)
return cv.Optional(name, default=self.get_default(name, fallback))
def get_dimensions(self, config) -> tuple[int, int]:
if CONF_DIMENSIONS in config:
# Explicit dimensions, just use as is
dimensions = config[CONF_DIMENSIONS]
if isinstance(dimensions, dict):
width = dimensions[CONF_WIDTH]
height = dimensions[CONF_HEIGHT]
else:
(width, height) = dimensions
else:
# Default dimensions, use model defaults
width = self.get_default(CONF_WIDTH)
height = self.get_default(CONF_HEIGHT)
return width, height
def extend(self, name, **kwargs) -> "EpaperModel":
"""
Extend the current model with additional parameters or a modified init sequence.
Parameters supplied here will override the defaults of the current model.
if the initsequence is not provided, the current model's initsequence will be used.
If add_init_sequence is provided, it will be appended to the current initsequence.
:param name:
:param kwargs:
:return:
"""
initsequence = list(kwargs.pop("initsequence", self.initsequence) or ())
initsequence.extend(kwargs.pop("add_init_sequence", ()))
defaults = self.defaults.copy()
defaults.update(kwargs)
return self.__class__(name, initsequence=tuple(initsequence), **defaults)

View File

@@ -1,51 +0,0 @@
from typing import Any
from . import EpaperModel
class SpectraE6(EpaperModel):
def __init__(self, name, class_name="EPaperSpectraE6", **kwargs):
super().__init__(name, class_name, **kwargs)
# fmt: off
def get_init_sequence(self, config: dict):
width, height = self.get_dimensions(config)
return (
(0xAA, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,),
(0x01, 0x3F,),
(0x00, 0x5F, 0x69,),
(0x03, 0x00, 0x54, 0x00, 0x44,),
(0x05, 0x40, 0x1F, 0x1F, 0x2C,),
(0x06, 0x6F, 0x1F, 0x17, 0x49,),
(0x08, 0x6F, 0x1F, 0x1F, 0x22,),
(0x30, 0x03,),
(0x50, 0x3F,),
(0x60, 0x02, 0x00,),
(0x61, width // 256, width % 256, height // 256, height % 256,),
(0x84, 0x01,),
(0xE3, 0x2F,),
)
def get_default(self, key, fallback: Any = False) -> Any:
return self.defaults.get(key, fallback)
spectra_e6 = SpectraE6("spectra-e6")
spectra_e6.extend(
"Seeed-reTerminal-E1002",
width=800,
height=480,
data_rate="20MHz",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)

View File

@@ -558,7 +558,6 @@ CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
# VFS requirement tracking # VFS requirement tracking
# Components that need VFS features can call require_vfs_select() or require_vfs_dir() # Components that need VFS features can call require_vfs_select() or require_vfs_dir()
@@ -655,9 +654,6 @@ FRAMEWORK_SCHEMA = cv.All(
): cv.boolean, ): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
} }
), ),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
@@ -930,10 +926,6 @@ async def to_code(config):
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})" f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
), ),
) )
add_idf_sdkconfig_option(
"CONFIG_ARDUINO_LOOP_STACK_SIZE",
conf[CONF_ADVANCED][CONF_LOOP_TASK_STACK_SIZE],
)
add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True) add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True) add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
@@ -1079,10 +1071,6 @@ async def to_code(config):
) )
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True) add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
cg.add_define(
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
)
cg.add_define( cg.add_define(
"USE_ESP_IDF_VERSION_CODE", "USE_ESP_IDF_VERSION_CODE",
cg.RawExpression( cg.RawExpression(

View File

@@ -1,6 +1,5 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "preferences.h" #include "preferences.h"
@@ -97,11 +96,7 @@ void loop_task(void *pv_params) {
extern "C" void app_main() { extern "C" void app_main() {
esp32::setup_preferences(); esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
#else
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
} }
#endif // USE_ESP_IDF #endif // USE_ESP_IDF

View File

@@ -7,7 +7,6 @@ from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import socket
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -22,7 +21,6 @@ from esphome.core import CORE, CoroPriority, TimePeriod, coroutine_with_priority
import esphome.final_validate as fv import esphome.final_validate as fv
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["socket"]
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"] CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
DOMAIN = "esp32_ble" DOMAIN = "esp32_ble"
@@ -483,11 +481,6 @@ async def to_code(config):
cg.add(var.set_name(name)) cg.add(var.set_name(name))
await cg.register_component(var, config) await cg.register_component(var, config)
# BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
# This enables low-latency (~12μs) BLE event processing instead of waiting for
# select() timeout (0-16ms). The wake socket is shared across all components.
socket.require_wake_loop_threadsafe()
# Define max connections for use in C++ code (e.g., ble_server.h) # Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS) max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections) cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)

View File

@@ -31,26 +31,6 @@ namespace esphome::esp32_ble {
static const char *const TAG = "esp32_ble"; static const char *const TAG = "esp32_ble";
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
#define GAP_SCAN_COMPLETE_EVENTS \
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
#define GAP_ADV_COMPLETE_EVENTS \
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
#define GAP_SECURITY_EVENTS \
case ESP_GAP_BLE_AUTH_CMPL_EVT: \
case ESP_GAP_BLE_SEC_REQ_EVT: \
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
case ESP_GAP_BLE_NC_REQ_EVT
void ESP32BLE::setup() { void ESP32BLE::setup() {
global_ble = this; global_ble = this;
if (!ble_pre_setup_()) { if (!ble_pre_setup_()) {
@@ -438,48 +418,60 @@ void ESP32BLE::loop() {
break; break;
// Scan complete events // Scan complete events
GAP_SCAN_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
// Advertising complete events case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// RSSI complete event // All three scan complete events have the same structure with just status
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// Security events // This is verified at compile-time by static_assert checks in ble_event.h
GAP_SECURITY_EVENTS: // The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
{ for (auto *gap_handler : this->gap_event_handlers_) {
esp_ble_gap_cb_param_t *param; gap_handler->gap_event_handler(
// clang-format off gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
switch (gap_event) { }
// All three scan complete events have the same structure with just status #endif
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe break;
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
GAP_SCAN_COMPLETE_EVENTS:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
break;
// All advertising complete events have the same structure with just status // Advertising complete events
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete); case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
break; case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
}
#endif
break;
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: // RSSI complete event
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete); case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
break; ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
}
#endif
break;
GAP_SECURITY_EVENTS: // Security events
param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security); case ESP_GAP_BLE_AUTH_CMPL_EVT:
break; case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
default: case ESP_GAP_BLE_PASSKEY_REQ_EVT:
break; case ESP_GAP_BLE_NC_REQ_EVT:
} ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
// clang-format on #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
// Dispatch to all registered handlers for (auto *gap_handler : this->gap_event_handlers_) {
for (auto *gap_handler : this->gap_event_handlers_) { gap_handler->gap_event_handler(
gap_handler->gap_event_handler(gap_event, param); gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
}
} }
#endif #endif
break; break;
@@ -559,22 +551,24 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
// Queue GAP events that components need to handle // Queue GAP events that components need to handle
// Scanning events - used by esp32_ble_tracker // Scanning events - used by esp32_ble_tracker
case ESP_GAP_BLE_SCAN_RESULT_EVT: case ESP_GAP_BLE_SCAN_RESULT_EVT:
GAP_SCAN_COMPLETE_EVENTS: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
// Advertising events - used by esp32_ble_beacon and esp32_ble server // Advertising events - used by esp32_ble_beacon and esp32_ble server
GAP_ADV_COMPLETE_EVENTS: case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// Connection events - used by ble_client // Connection events - used by ble_client
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
enqueue_ble_event(event, param);
return;
// Security events - used by ble_client and bluetooth_proxy // Security events - used by ble_client and bluetooth_proxy
// These are rare but interactive (pairing/bonding), so notify immediately case ESP_GAP_BLE_AUTH_CMPL_EVT:
GAP_SECURITY_EVENTS: case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
enqueue_ble_event(event, param); enqueue_ble_event(event, param);
// Wake up main loop to process security event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
return; return;
// Ignore these GAP events as they are not relevant for our use case // Ignore these GAP events as they are not relevant for our use case
@@ -594,10 +588,6 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
enqueue_ble_event(event, gatts_if, param); enqueue_ble_event(event, gatts_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif
@@ -605,10 +595,6 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
enqueue_ble_event(event, gattc_if, param); enqueue_ble_event(event, gattc_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
#endif #endif

View File

@@ -163,11 +163,6 @@ class ESP32BLE : public Component {
void advertising_init_(); void advertising_init_();
#endif #endif
// BLE uses the core wake_loop_threadsafe() mechanism to wake the main event loop
// from BLE tasks. This enables low-latency (~12μs) event processing instead of
// waiting for select() timeout (0-16ms). The wake socket is shared with other
// components that need this functionality.
private: private:
template<typename... Args> friend void enqueue_ble_event(Args... args); template<typename... Args> friend void enqueue_ble_event(Args... args);
@@ -215,17 +210,17 @@ extern ESP32BLE *global_ble;
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> { template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
public: public:
bool check(const Ts &...x) override { return global_ble->is_active(); } bool check(Ts... x) override { return global_ble->is_active(); }
}; };
template<typename... Ts> class BLEEnableAction : public Action<Ts...> { template<typename... Ts> class BLEEnableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->enable(); } void play(Ts... x) override { global_ble->enable(); }
}; };
template<typename... Ts> class BLEDisableAction : public Action<Ts...> { template<typename... Ts> class BLEDisableAction : public Action<Ts...> {
public: public:
void play(const Ts &...x) override { global_ble->disable(); } void play(Ts... x) override { global_ble->disable(); }
}; };
} // namespace esphome::esp32_ble } // namespace esphome::esp32_ble

View File

@@ -71,7 +71,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {} BLECharacteristicSetValueAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { void play(Ts... x) override {
// If the listener is already set, do nothing // If the listener is already set, do nothing
if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_)) if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
return; return;
@@ -96,7 +96,7 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> { template<typename... Ts> class BLECharacteristicNotifyAction : public Action<Ts...> {
public: public:
BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {} BLECharacteristicNotifyAction(BLECharacteristic *characteristic) : parent_(characteristic) {}
void play(const Ts &...x) override { void play(Ts... x) override {
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION #ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
// Call the pre-notify event // Call the pre-notify event
BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_); BLECharacteristicSetValueActionManager::get_instance()->emit_pre_notify(this->parent_);
@@ -116,7 +116,7 @@ template<typename... Ts> class BLEDescriptorSetValueAction : public Action<Ts...
BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {} BLEDescriptorSetValueAction(BLEDescriptor *descriptor) : parent_(descriptor) {}
TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer) TEMPLATABLE_VALUE(std::vector<uint8_t>, buffer)
void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); } void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
void play(const Ts &...x) override { this->parent_->set_value(this->buffer_.value(x...)); } void play(Ts... x) override { this->parent_->set_value(this->buffer_.value(x...)); }
protected: protected:
BLEDescriptor *parent_; BLEDescriptor *parent_;

View File

@@ -96,7 +96,7 @@ template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
public: public:
ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(bool, continuous) TEMPLATABLE_VALUE(bool, continuous)
void play(const Ts &...x) override { void play(Ts... x) override {
this->parent_->set_scan_continuous(this->continuous_.value(x...)); this->parent_->set_scan_continuous(this->continuous_.value(x...));
this->parent_->start_scan(); this->parent_->start_scan();
} }
@@ -107,7 +107,7 @@ template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> {
template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, public Parented<ESP32BLETracker> { template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, public Parented<ESP32BLETracker> {
public: public:
void play(const Ts &...x) override { this->parent_->stop_scan(); } void play(Ts... x) override { this->parent_->stop_scan(); }
}; };
} // namespace esphome::esp32_ble_tracker } // namespace esphome::esp32_ble_tracker

View File

@@ -4,7 +4,6 @@ from esphome import automation, pins
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import i2c from esphome.components import i2c
from esphome.components.esp32 import add_idf_component from esphome.components.esp32 import add_idf_component
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_BRIGHTNESS, CONF_BRIGHTNESS,
@@ -27,9 +26,10 @@ import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"] DEPENDENCIES = ["esp32"]
AUTO_LOAD = ["camera", "psram"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase)
ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData") ESP32CameraImageData = esp32_camera_ns.struct("CameraImageData")
@@ -163,14 +163,6 @@ CONF_ON_IMAGE = "on_image"
camera_range_param = cv.int_range(min=-2, max=2) camera_range_param = cv.int_range(min=-2, max=2)
def validate_fb_location_(value):
validator = cv.enum(ENUM_FB_LOCATION, upper=True)
if value.lower() == psram_domain:
validator = cv.All(validator, cv.requires_component(psram_domain))
return validator(value)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.ENTITY_BASE_SCHEMA.extend( cv.ENTITY_BASE_SCHEMA.extend(
{ {
@@ -244,9 +236,9 @@ CONFIG_SCHEMA = cv.All(
cv.framerate, cv.Range(min=0, max=1) cv.framerate, cv.Range(min=0, max=1)
), ),
cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2),
cv.Optional( cv.Optional(CONF_FRAME_BUFFER_LOCATION, default="PSRAM"): cv.enum(
CONF_FRAME_BUFFER_LOCATION, default="PSRAM" ENUM_FB_LOCATION, upper=True
): validate_fb_location_, ),
cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( cv.Optional(CONF_ON_STREAM_START): automation.validate_automation(
{ {
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(

View File

@@ -1,78 +0,0 @@
import hashlib
from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
import esphome.config_validation as cv
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
from esphome.core import CORE, HexInt
CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
Esp32HostedUpdate = esp32_hosted_ns.class_(
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
)
def _validate_sha256(value: Any) -> str:
value = cv.string_strict(value)
if len(value) != 64:
raise cv.Invalid("SHA256 must be 64 hexadecimal characters")
try:
bytes.fromhex(value)
except ValueError as e:
raise cv.Invalid(f"SHA256 must be valid hexadecimal: {e}") from e
return value
CONFIG_SCHEMA = cv.All(
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
{
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
),
esp32.only_on_variant(
supported=[
esp32.const.VARIANT_ESP32H2,
esp32.const.VARIANT_ESP32P4,
]
),
)
def _validate_firmware(config: dict[str, Any]) -> None:
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
firmware_data = f.read()
calculated = hashlib.sha256(firmware_data).hexdigest()
expected = config[CONF_SHA256].lower()
if calculated != expected:
raise cv.Invalid(
f"SHA256 mismatch for {config[CONF_PATH]}: expected {expected}, got {calculated}"
)
FINAL_VALIDATE_SCHEMA = _validate_firmware
async def to_code(config: dict[str, Any]) -> None:
var = await update.new_update(config)
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
await cg.register_component(var, config)

View File

@@ -1,164 +0,0 @@
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esp32_hosted_update.h"
#include "esphome/components/watchdog/watchdog.h"
#include "esphome/components/sha256/sha256.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include <esp_image_format.h>
#include <esp_app_desc.h>
#include <esp_hosted.h>
extern "C" {
#include <esp_hosted_ota.h>
}
namespace esphome::esp32_hosted {
static const char *const TAG = "esp32_hosted.update";
// older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
} else {
this->update_info_.current_version = "unknown";
}
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
// get image version
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
if (app_desc->magic_word == ESP_APP_DESC_MAGIC_WORD) {
ESP_LOGD(TAG, "Firmware version: %s", app_desc->version);
ESP_LOGD(TAG, "Project name: %s", app_desc->project_name);
ESP_LOGD(TAG, "Build date: %s", app_desc->date);
ESP_LOGD(TAG, "Build time: %s", app_desc->time);
ESP_LOGD(TAG, "IDF version: %s", app_desc->idf_ver);
this->update_info_.latest_version = app_desc->version;
if (this->update_info_.latest_version != this->update_info_.current_version) {
this->state_ = update::UPDATE_STATE_AVAILABLE;
} else {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
ESP_APP_DESC_MAGIC_WORD);
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Firmware too small to contain app description");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
// publish state
this->status_clear_error();
this->publish_state();
}
void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 Hosted Update:\n"
" Current Version: %s\n"
" Latest Version: %s\n"
" Latest Size: %zu bytes",
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
this->firmware_size_);
}
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
ESP_LOGE(TAG, "No firmware data available");
return;
}
sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
this->status_set_error("SHA256 verification failed");
this->publish_state();
return;
}
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
watchdog::WatchdogManager watchdog(20000);
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to begin OTA");
this->publish_state();
return;
}
uint8_t chunk[CHUNK_SIZE];
const uint8_t *data_ptr = this->firmware_data_;
size_t remaining = this->firmware_size_;
while (remaining > 0) {
size_t chunk_size = std::min(remaining, static_cast<size_t>(CHUNK_SIZE));
memcpy(chunk, data_ptr, chunk_size);
err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error("Failed to write OTA data");
this->publish_state();
return;
}
data_ptr += chunk_size;
remaining -= chunk_size;
App.feed_wdt();
}
err = esp_hosted_slave_ota_end(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to end OTA");
this->publish_state();
return;
}
// activate new firmware
err = esp_hosted_slave_ota_activate(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error("Failed to activate OTA");
this->publish_state();
return;
}
// update state
ESP_LOGI(TAG, "OTA update successful");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
this->status_clear_error();
this->publish_state();
// schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });
}
} // namespace esphome::esp32_hosted
#endif

View File

@@ -1,32 +0,0 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4)
#include "esphome/core/component.h"
#include "esphome/components/update/update_entity.h"
#include <array>
namespace esphome::esp32_hosted {
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void perform(bool force) override;
void check() override {}
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
protected:
const uint8_t *firmware_data_{nullptr};
size_t firmware_size_{0};
std::array<uint8_t, 32> firmware_sha256_;
};
} // namespace esphome::esp32_hosted
#endif

View File

@@ -40,7 +40,7 @@ template<typename... Ts> class SetFrequencyAction : public Action<Ts...> {
SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {} SetFrequencyAction(ESP8266PWM *parent) : parent_(parent) {}
TEMPLATABLE_VALUE(float, frequency); TEMPLATABLE_VALUE(float, frequency);
void play(const Ts &...x) { void play(Ts... x) {
float freq = this->frequency_.value(x...); float freq = this->frequency_.value(x...);
this->parent_->update_frequency(freq); this->parent_->update_frequency(freq);
} }

View File

@@ -34,7 +34,7 @@ template<typename... Ts> class AdjustAction : public Action<Ts...> {
TEMPLATABLE_VALUE(float, voltage) TEMPLATABLE_VALUE(float, voltage)
void play(const Ts &...x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); } void play(Ts... x) override { this->ldo_->adjust_voltage(this->voltage_.value(x...)); }
protected: protected:
EspLdo *ldo_; EspLdo *ldo_;

View File

@@ -94,7 +94,7 @@ void ESPHomeOTAComponent::dump_config() {
"Over-The-Air updates:\n" "Over-The-Air updates:\n"
" Address: %s:%u\n" " Address: %s:%u\n"
" Version: %d", " Version: %d",
network::get_use_address(), this->port_, USE_OTA_VERSION); network::get_use_address().c_str(), this->port_, USE_OTA_VERSION);
#ifdef USE_OTA_PASSWORD #ifdef USE_OTA_PASSWORD
if (!this->password_.empty()) { if (!this->password_.empty()) {
ESP_LOGCONFIG(TAG, " Password configured"); ESP_LOGCONFIG(TAG, " Password configured");
@@ -281,15 +281,19 @@ void ESPHomeOTAComponent::handle_data_() {
#endif #endif
// Acknowledge auth OK - 1 byte // Acknowledge auth OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); buf[0] = ota::OTA_RESPONSE_AUTH_OK;
this->writeall_(buf, 1);
// Read size, 4 bytes MSB first // Read size, 4 bytes MSB first
if (!this->readall_(buf, 4)) { if (!this->readall_(buf, 4)) {
this->log_read_error_(LOG_STR("size")); this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto) goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} }
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) | ota_size = 0;
(static_cast<size_t>(buf[2]) << 8) | buf[3]; for (uint8_t i = 0; i < 4; i++) {
ota_size <<= 8;
ota_size |= buf[i];
}
ESP_LOGV(TAG, "Size is %u bytes", ota_size); ESP_LOGV(TAG, "Size is %u bytes", ota_size);
// Now that we've passed authentication and are actually // Now that we've passed authentication and are actually
@@ -309,7 +313,8 @@ void ESPHomeOTAComponent::handle_data_() {
update_started = true; update_started = true;
// Acknowledge prepare OK - 1 byte // Acknowledge prepare OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
this->writeall_(buf, 1);
// Read binary MD5, 32 bytes // Read binary MD5, 32 bytes
if (!this->readall_(buf, 32)) { if (!this->readall_(buf, 32)) {
@@ -321,7 +326,8 @@ void ESPHomeOTAComponent::handle_data_() {
this->backend_->set_update_md5(sbuf); this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte // Acknowledge MD5 OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
this->writeall_(buf, 1);
while (total < ota_size) { while (total < ota_size) {
// TODO: timeout check // TODO: timeout check
@@ -348,7 +354,8 @@ void ESPHomeOTAComponent::handle_data_() {
total += read; total += read;
#if USE_OTA_VERSION == 2 #if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
this->writeall_(buf, 1);
size_acknowledged += OTA_BLOCK_SIZE; size_acknowledged += OTA_BLOCK_SIZE;
} }
#endif #endif
@@ -367,7 +374,8 @@ void ESPHomeOTAComponent::handle_data_() {
} }
// Acknowledge receive OK - 1 byte // Acknowledge receive OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
this->writeall_(buf, 1);
error_code = this->backend_->end(); error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) { if (error_code != ota::OTA_RESPONSE_OK) {
@@ -376,7 +384,8 @@ void ESPHomeOTAComponent::handle_data_() {
} }
// Acknowledge Update end OK - 1 byte // Acknowledge Update end OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
this->writeall_(buf, 1);
// Read ACK // Read ACK
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
@@ -395,7 +404,8 @@ void ESPHomeOTAComponent::handle_data_() {
App.safe_reboot(); App.safe_reboot();
error: error:
this->write_byte_(static_cast<uint8_t>(error_code)); buf[0] = static_cast<uint8_t>(error_code);
this->writeall_(buf, 1);
this->cleanup_connection_(); this->cleanup_connection_();
if (this->backend_ != nullptr && update_started) { if (this->backend_ != nullptr && update_started) {

View File

@@ -53,7 +53,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
#endif // USE_OTA_PASSWORD #endif // USE_OTA_PASSWORD
bool readall_(uint8_t *buf, size_t len); bool readall_(uint8_t *buf, size_t len);
bool writeall_(const uint8_t *buf, size_t len); bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
bool try_read_(size_t to_read, const LogString *desc); bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc); bool try_write_(size_t to_write, const LogString *desc);

View File

@@ -1,6 +1,6 @@
from esphome import automation, core from esphome import automation, core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import socket, wifi from esphome.components import wifi
from esphome.components.udp import CONF_ON_RECEIVE from esphome.components.udp import CONF_ON_RECEIVE
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
@@ -17,7 +17,6 @@ from esphome.core import CORE, HexInt
from esphome.types import ConfigType from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"] CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["socket"]
byte_vector = cg.std_vector.template(cg.uint8) byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6) peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -121,10 +120,6 @@ async def to_code(config):
if CORE.using_arduino: if CORE.using_arduino:
cg.add_library("WiFi", None) cg.add_library("WiFi", None)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()
cg.add_define("USE_ESPNOW") cg.add_define("USE_ESPNOW")
if wifi_channel := config.get(CONF_CHANNEL): if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel)) cg.add(var.set_wifi_channel(wifi_channel))

View File

@@ -36,7 +36,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; } void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; }
void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; } void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; }
void play_complex(const Ts &...x) override { void play_complex(Ts... x) override {
this->num_running_++; this->num_running_++;
send_callback_t send_callback = [this, x...](esp_err_t status) { send_callback_t send_callback = [this, x...](esp_err_t status) {
if (status == ESP_OK) { if (status == ESP_OK) {
@@ -67,7 +67,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
} }
} }
void play(const Ts &...x) override { /* ignore - see play_complex */ void play(Ts... x) override { /* ignore - see play_complex */
} }
void stop() override { void stop() override {
@@ -90,7 +90,7 @@ template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Pare
TEMPLATABLE_VALUE(peer_address_t, address); TEMPLATABLE_VALUE(peer_address_t, address);
public: public:
void play(const Ts &...x) override { void play(Ts... x) override {
peer_address_t address = this->address_.value(x...); peer_address_t address = this->address_.value(x...);
this->parent_->add_peer(address.data()); this->parent_->add_peer(address.data());
} }
@@ -100,7 +100,7 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P
TEMPLATABLE_VALUE(peer_address_t, address); TEMPLATABLE_VALUE(peer_address_t, address);
public: public:
void play(const Ts &...x) override { void play(Ts... x) override {
peer_address_t address = this->address_.value(x...); peer_address_t address = this->address_.value(x...);
this->parent_->del_peer(address.data()); this->parent_->del_peer(address.data());
} }
@@ -109,7 +109,7 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> { template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
public: public:
TEMPLATABLE_VALUE(uint8_t, channel) TEMPLATABLE_VALUE(uint8_t, channel)
void play(const Ts &...x) override { void play(Ts... x) override {
if (this->parent_->is_wifi_enabled()) { if (this->parent_->is_wifi_enabled()) {
return; return;
} }

View File

@@ -4,7 +4,6 @@
#include "espnow_err.h" #include "espnow_err.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -98,11 +97,6 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
// Push the packet to the queue // Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet); global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size // Push always because we're the only producer and the pool ensures we never exceed queue size
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) { void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
@@ -120,11 +114,6 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
// Push the packet to the queue // Push the packet to the queue
global_esp_now->receive_packet_queue_.push(packet); global_esp_now->receive_packet_queue_.push(packet);
// Push always because we're the only producer and the pool ensures we never exceed queue size // Push always because we're the only producer and the pool ensures we never exceed queue size
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
} }
ESPNowComponent::ESPNowComponent() { global_esp_now = this; } ESPNowComponent::ESPNowComponent() { global_esp_now = this; }

View File

@@ -691,9 +691,9 @@ void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_
// set_use_address() is guaranteed to be called during component setup by Python code generation, // set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed. // so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const char *EthernetComponent::get_use_address() const { return this->use_address_; } const std::string &EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; } void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; }
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) { void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err; esp_err_t err;

View File

@@ -88,8 +88,8 @@ class EthernetComponent : public Component {
network::IPAddresses get_ip_addresses(); network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num); network::IPAddress get_dns_address(uint8_t num);
const char *get_use_address() const; const std::string &get_use_address() const;
void set_use_address(const char *use_address); void set_use_address(const std::string &use_address);
void get_eth_mac_address_raw(uint8_t *mac); void get_eth_mac_address_raw(uint8_t *mac);
std::string get_eth_mac_address_pretty(); std::string get_eth_mac_address_pretty();
eth_duplex_t get_duplex_mode(); eth_duplex_t get_duplex_mode();
@@ -114,6 +114,7 @@ class EthernetComponent : public Component {
/// @brief Set arbitratry PHY registers from config. /// @brief Set arbitratry PHY registers from config.
void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data); void write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data);
std::string use_address_;
#ifdef USE_ETHERNET_SPI #ifdef USE_ETHERNET_SPI
uint8_t clk_pin_; uint8_t clk_pin_;
uint8_t miso_pin_; uint8_t miso_pin_;
@@ -157,11 +158,6 @@ class EthernetComponent : public Component {
esp_eth_handle_t eth_handle_; esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr}; esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_; optional<std::array<uint8_t, 6>> fixed_mac_;
private:
// Stores a pointer to a string literal (static storage duration).
// ONLY set from Python-generated code with string literals - never dynamic strings.
const char *use_address_{""};
}; };
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)

View File

@@ -85,6 +85,11 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema) return _EVENT_SCHEMA.extend(schema)
# Remove before 2025.11.0
EVENT_SCHEMA = event_schema()
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
async def setup_event_core_(var, config, *, event_types: list[str]): async def setup_event_core_(var, config, *, event_types: list[str]):
await setup_entity(var, config, "event") await setup_entity(var, config, "event")

View File

@@ -11,7 +11,7 @@ template<typename... Ts> class TriggerEventAction : public Action<Ts...>, public
public: public:
TEMPLATABLE_VALUE(std::string, event_type) TEMPLATABLE_VALUE(std::string, event_type)
void play(const Ts &...x) override { this->parent_->trigger(this->event_type_.value(x...)); } void play(Ts... x) override { this->parent_->trigger(this->event_type_.value(x...)); }
}; };
class EventTrigger : public Trigger<std::string> { class EventTrigger : public Trigger<std::string> {

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