1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-10 11:55:52 +00:00

Compare commits

..

64 Commits

Author SHA1 Message Date
J. Nick Koston
9913d7d9e9 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 19:01:26 -05:00
J. Nick Koston
119dc8b530 flatten 2025-10-29 19:01:12 -05:00
J. Nick Koston
3c8f4c1d5d Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:58:30 -05:00
J. Nick Koston
4892638768 flatten 2025-10-29 18:58:07 -05:00
J. Nick Koston
306448c02c Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:54:31 -05:00
J. Nick Koston
eb73a23ab1 flatten 2025-10-29 18:53:50 -05:00
J. Nick Koston
22cb0fbb70 flatten 2025-10-29 18:53:35 -05:00
J. Nick Koston
04d22becd0 Merge branch 'determine_jobs_comp_split' into determine_jobs_comp_split_test 2025-10-29 18:48:14 -05:00
J. Nick Koston
36a7951f7a fix 2025-10-29 18:48:00 -05:00
J. Nick Koston
81382db3d0 DNM: ci testing 2025-10-29 18:45:29 -05:00
J. Nick Koston
e0b02ccfc5 [ci] Consolidate component splitting into determine-jobs 2025-10-29 18:43:49 -05:00
Stuart Parmenter
918650f15a [lvgl] memset canvas buffer to prevent display of random garbage (#11582)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 21:06:45 +00:00
Stuart Parmenter
287f65cbaf [lvgl] fix typo from previous refactor (#11596) 2025-10-30 07:27:31 +11:00
Javier Peletier
f18c70a256 [core] Fix substitution id redefinition false positive (#11603) 2025-10-30 07:06:55 +13:00
Jonathan Swoboda
6fb490f49b [remote_transmitter] Add non-blocking mode (#11524) 2025-10-29 12:40:22 -04:00
J. Nick Koston
66cf7c3a3b [lvgl] Fix nested lambdas in automations unable to access parameters (#11583)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-10-29 17:07:48 +11:00
dependabot[bot]
f29021b5ef Bump ruff from 0.14.1 to 0.14.2 (#11519)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2025-10-29 05:23:42 +00:00
dependabot[bot]
7549ca4d39 Bump actions/download-artifact from 5.0.0 to 6.0.0 (#11521)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:13 -05:00
dependabot[bot]
33e7a2101b Bump actions/upload-artifact from 4.6.2 to 5.0.0 (#11520)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:20:05 -05:00
dependabot[bot]
59a216bfcb Bump github/codeql-action from 4.30.9 to 4.31.0 (#11522)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:19:47 -05:00
Jesse Hills
09d89000ad [core] Remove deprecated schema constants (#11591) 2025-10-29 00:14:02 -05:00
Kent Gibson
b6c9ece0e6 template_alarm_control_panel readability improvements (#11593) 2025-10-29 00:10:36 -05:00
dependabot[bot]
7169556722 Bump aioesphomeapi from 42.4.0 to 42.5.0 (#11597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 04:46:47 +00:00
J. Nick Koston
f6e4c0cb52 [ci] Fix component tests not running when only test files change (#11580) 2025-10-29 16:22:28 +13:00
J. Nick Koston
f3634edc22 [select] Store options in flash to reduce RAM usage (#11514) 2025-10-29 15:28:16 +13:00
Jesse Hills
a609343cb6 [fan] Remove deprecated set_speed function (#11590) 2025-10-28 21:06:30 -05:00
Clyde Stubbs
5528c3c765 [mipi_rgb] Fix rotation with custom model (#11585) 2025-10-29 14:37:14 +13:00
Anton Sergunov
0d805355f5 Fix the LiberTiny bug with UART pin setup (#11518) 2025-10-29 14:33:16 +13:00
Jesse Hills
99f48ae51c [logger] Improve level validation errors (#11589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-29 01:29:40 +00:00
Jesse Hills
25e4aafd71 [ci] Fix auto labeller workflow with wrong comment for too-big with labels (#11592) 2025-10-29 14:28:29 +13:00
Kent Gibson
4f2d54be4e template_alarm_control_panel cleanups (#11469) 2025-10-29 13:48:26 +13:00
dependabot[bot]
249cd7415b Bump aioesphomeapi from 42.3.0 to 42.4.0 (#11586)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-10-29 00:32:41 +00:00
J. Nick Koston
78d780105b [ci] Change upper Python version being tested to 3.13 (#11587) 2025-10-28 19:24:37 -05:00
Jesse Hills
466d4522bc [http_request] Pass trigger variables into on_response/on_error (#11464) 2025-10-29 12:17:16 +13:00
Javier Peletier
e462217500 [packages] Tighten package validation (#11584) 2025-10-29 11:18:47 +13:00
J. Nick Koston
f1bce262ed [uart] Optimize UART components to eliminate temporary vector allocations (#11570)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-29 09:48:20 +13:00
J. Nick Koston
7ed7e7ad26 [climate] Replace std::set with FiniteSetMask for trait storage (#11466) 2025-10-29 08:46:44 +13:00
J. Nick Koston
08b8454555 [ble_client] Use function pointers for lambda actions and sensors (#11564)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-10-29 08:10:32 +13:00
J. Nick Koston
0119e17f04 [ci] Remove base bus components exclusion from memory impact analysis (#11572) 2025-10-29 08:08:13 +13:00
J. Nick Koston
c3f40de844 [modbus_controller] Optimize lambdas to use function pointers instead of std::function (#11566) 2025-10-29 08:06:13 +13:00
J. Nick Koston
7dd829cfca [esp32_ble_server][esp32_improv] Eliminate unnecessary heap allocations (#11569) 2025-10-29 08:05:12 +13:00
J. Nick Koston
da19673f51 Add additional uart test coverage (#11571) 2025-10-29 08:03:09 +13:00
rwrozelle
f5e32d03d0 [http_request] update timeout to be uint32_t (#11577) 2025-10-28 12:41:48 -04:00
J. Nick Koston
f3b69383fd Add additional modbus compile tests (#11567) 2025-10-28 16:43:16 +13:00
J. Nick Koston
aba72809d3 Additional tests for ble_client lambdas (#11565) 2025-10-28 16:43:10 +13:00
aanban
85205a28d2 [remote_base] add support for Dyson cool AM07 tower fan (#10163)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2025-10-27 22:49:16 -04:00
Edward Firmo
285e006637 [nextion] Add set_component_visibility() method for dynamic visibility control (#11530)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2025-10-28 13:22:28 +13:00
Edward Firmo
5647f36900 [nextion] Remove TFT upload baud rate validation to reduce flash usage (#11012) 2025-10-28 13:21:17 +13:00
Samuel Sieb
1e9309ffff [tuya] allow enum for eco id (#11544)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-10-28 13:20:21 +13:00
Daniel Herrmann
ce8a6a6c43 fix: load_cert_chain requires the path, not a file object (#11543) 2025-10-28 12:24:13 +13:00
J. Nick Koston
dfb4b31bf9 [template] Store initial option as index in template select (#11523) 2025-10-28 11:37:40 +13:00
J. Nick Koston
31b1b50ad9 [number] Skip set_mode call when using default AUTO mode (#11537) 2025-10-28 11:16:38 +13:00
J. Nick Koston
3377080272 [core] Simplify ESPTime::strftime() and save 20 bytes flash (#11539) 2025-10-28 11:16:09 +13:00
Keith Burzinski
d65ad69338 [uart] Fix order of initialization calls (#11510) 2025-10-27 17:09:45 -05:00
J. Nick Koston
dfa69173ea [api] Use FixedVector const references for service array arguments (#11546) 2025-10-28 11:03:44 +13:00
J. Nick Koston
f44615cc8d [template] Optimize all template platforms to use function pointers for stateless lambdas (#11555) 2025-10-28 11:00:02 +13:00
J. Nick Koston
bda4769bd3 [core] Optimize TemplatableValue to use function pointers for stateless lambdas (#11554) 2025-10-27 21:05:40 +00:00
J. Nick Koston
14b057f54e [light] Optimize LambdaLightEffect and AddressableLambdaLightEffect with function pointers (#11556) 2025-10-27 20:14:16 +00:00
J. Nick Koston
e26b5874d7 [api] Register user services with initializer_list (#11545) 2025-10-28 09:07:31 +13:00
J. Nick Koston
00f22e5c36 [network] Eliminate runtime string parsing for IP address initialization (#11561) 2025-10-28 08:51:08 +13:00
Javier Peletier
51e080c2d3 [substitutions] fix #11077 Preserve ESPHomeDatabase (document metadata) in substitutions (#11087)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-10-27 19:46:26 +00:00
J. Nick Koston
3c18558003 Optimize stateless lambdas to use function pointers (#11551) 2025-10-28 08:06:22 +13:00
Jonathan Swoboda
7394cbf773 [core] Don't allow python 3.14 (#11527) 2025-10-26 09:00:08 -04:00
J. Nick Koston
1577a46efd [gpio] Skip set_inverted() call for default false value (#11538) 2025-10-25 22:09:42 -07:00
190 changed files with 1971 additions and 1784 deletions

View File

@@ -416,7 +416,7 @@ jobs:
}
// Generate review messages
function generateReviewMessages(finalLabels) {
function generateReviewMessages(finalLabels, originalLabelCount) {
const messages = [];
const prAuthor = context.payload.pull_request.user.login;
@@ -430,15 +430,15 @@ jobs:
.reduce((sum, file) => sum + (file.deletions || 0), 0);
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
const tooManyLabels = finalLabels.length > MAX_LABELS;
const tooManyLabels = originalLabelCount > MAX_LABELS;
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) {
message += `This PR affects ${finalLabels.length} different components/areas.`;
message += `This PR affects ${originalLabelCount} different components/areas.`;
} else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
}
@@ -466,8 +466,8 @@ jobs:
}
// Handle reviews
async function handleReviews(finalLabels) {
const reviewMessages = generateReviewMessages(finalLabels);
async function handleReviews(finalLabels, originalLabelCount) {
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
const hasReviewableLabels = finalLabels.some(label =>
['too-big', 'needs-codeowners'].includes(label)
);
@@ -627,6 +627,7 @@ jobs:
// Handle too many labels (only for non-mega PRs)
const tooManyLabels = finalLabels.length > MAX_LABELS;
const originalLabelCount = finalLabels.length;
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
finalLabels = ['too-big'];
@@ -635,7 +636,7 @@ jobs:
console.log('Computed labels:', finalLabels.join(', '));
// Handle reviews
await handleReviews(finalLabels);
await handleReviews(finalLabels, originalLabelCount);
// Apply labels
if (finalLabels.length > 0) {

View File

@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: generated-proto-files
path: |

View File

@@ -114,7 +114,7 @@ jobs:
matrix:
python-version:
- "3.11"
- "3.14"
- "3.13"
os:
- ubuntu-latest
- macOS-latest
@@ -123,9 +123,9 @@ jobs:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.14"
- python-version: "3.13"
os: windows-latest
- python-version: "3.14"
- python-version: "3.13"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
@@ -180,6 +180,7 @@ jobs:
memory_impact: ${{ steps.determine.outputs.memory-impact }}
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -214,6 +215,7 @@ jobs:
echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
integration-tests:
name: Run integration tests
@@ -536,59 +538,18 @@ jobs:
run: script/ci-suggest-changes
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:
name: Test components batch (${{ matrix.components }})
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
- test-build-components-splitter
if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
strategy:
fail-fast: false
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
matrix:
components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
steps:
- name: Show disk space
run: |
@@ -849,7 +810,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -913,7 +874,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -943,13 +904,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -980,7 +941,6 @@ jobs:
- clang-tidy-nosplit
- clang-tidy-split
- determine-jobs
- test-build-components-splitter
- test-build-components-split
- pre-commit-ci-lite
- memory-impact-target-branch

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
with:
category: "/language:${{matrix.language}}"

View File

@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -171,7 +171,7 @@ jobs:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Download digests
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: digests-*
path: /tmp/digests

View File

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

View File

@@ -731,13 +731,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1199,17 +1192,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -284,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

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

View File

@@ -172,12 +172,6 @@ def alarm_control_panel_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(
{
cv.GenerateID(): cv.use_id(AlarmControlPanel),

View File

@@ -71,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
"int": cg.int32,
"float": float,
"string": cg.std_string,
"bool[]": cg.std_vector.template(bool),
"int[]": cg.std_vector.template(cg.int32),
"float[]": cg.std_vector.template(float),
"string[]": cg.std_vector.template(cg.std_string),
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
"string[]": cg.FixedVector.template(cg.std_string)
.operator("const")
.operator("ref"),
}
CONF_ENCRYPTION = "encryption"
CONF_BATCH_DELAY = "batch_delay"

View File

@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"];
repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
}
// Deprecated in API version 1.6 - only used in deprecated fields

View File

@@ -1,3 +1,5 @@
// random comment
#include "api_connection.h"
#ifdef USE_API
#ifdef USE_API_NOISE

View File

@@ -142,11 +142,6 @@ APIError APINoiseFrameHelper::loop() {
* errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
*/
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
if (rx_header_buf_len_ < 3) {
// no header information yet

View File

@@ -54,11 +54,6 @@ APIError APIPlaintextFrameHelper::loop() {
* error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of 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
while (!rx_header_parsed_) {
// Now that we know when the socket is ready, we can read up to 3 bytes

View File

@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_speed{false};
bool supports_direction{false};
int32_t supported_speed_count{0};
const std::vector<std::string> *supported_preset_modes{};
const std::set<std::string> *supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

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

View File

@@ -548,11 +548,6 @@ def binary_sensor_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):
await setup_entity(var, config, "binary_sensor")

View File

@@ -96,8 +96,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
BLEClientWriteAction(BLEClient *ble_client) {
ble_client->register_ble_node(this);
ble_client_ = ble_client;
this->construct_simple_value_();
}
~BLEClientWriteAction() { this->destroy_simple_value_(); }
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
@@ -106,14 +109,18 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
this->value_template_ = std::move(func);
has_simple_value_ = false;
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
this->destroy_simple_value_();
this->value_.template_func = func;
this->has_simple_value_ = false;
}
void set_value_simple(const std::vector<uint8_t> &value) {
this->value_simple_ = value;
has_simple_value_ = true;
if (!this->has_simple_value_) {
this->construct_simple_value_();
}
this->value_.simple = value;
this->has_simple_value_ = true;
}
void play(Ts... x) override {}
@@ -121,7 +128,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(Ts... x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!write(value))
this->play_next_(x...);
@@ -194,10 +201,22 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
}
private:
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
void destroy_simple_value_() {
if (this->has_simple_value_) {
this->value_.simple.~vector();
}
}
BLEClient *ble_client_;
bool has_simple_value_ = true;
std::vector<uint8_t> value_simple_;
std::function<std::vector<uint8_t>(Ts...)> value_template_{};
union Value {
std::vector<uint8_t> simple;
std::vector<uint8_t> (*template_func)(Ts...);
Value() {} // trivial constructor
~Value() {} // trivial destructor - we manage lifetime via discriminator
} value_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;
std::tuple<Ts...> var_{};
@@ -213,9 +232,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
void play(Ts... x) override {
uint32_t passkey;
if (has_simple_value_) {
passkey = this->value_simple_;
passkey = this->value_.simple;
} else {
passkey = this->value_template_(x...);
passkey = this->value_.template_func(x...);
}
if (passkey > 999999)
return;
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
esp_ble_passkey_reply(remote_bda, true, passkey);
}
void set_value_template(std::function<uint32_t(Ts...)> func) {
this->value_template_ = std::move(func);
has_simple_value_ = false;
void set_value_template(uint32_t (*func)(Ts...)) {
this->value_.template_func = func;
this->has_simple_value_ = false;
}
void set_value_simple(const uint32_t &value) {
this->value_simple_ = value;
has_simple_value_ = true;
this->value_.simple = value;
this->has_simple_value_ = true;
}
private:
BLEClient *parent_{nullptr};
bool has_simple_value_ = true;
uint32_t value_simple_{0};
std::function<uint32_t(Ts...)> value_template_{};
union {
uint32_t simple;
uint32_t (*template_func)(Ts...);
} value_{.simple = 0};
};
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
@@ -249,27 +270,29 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
esp_bd_addr_t remote_bda;
memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
if (has_simple_value_) {
esp_ble_confirm_reply(remote_bda, this->value_simple_);
esp_ble_confirm_reply(remote_bda, this->value_.simple);
} else {
esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
}
}
void set_value_template(std::function<bool(Ts...)> func) {
this->value_template_ = std::move(func);
has_simple_value_ = false;
void set_value_template(bool (*func)(Ts...)) {
this->value_.template_func = func;
this->has_simple_value_ = false;
}
void set_value_simple(const bool &value) {
this->value_simple_ = value;
has_simple_value_ = true;
this->value_.simple = value;
this->has_simple_value_ = true;
}
private:
BLEClient *parent_{nullptr};
bool has_simple_value_ = true;
bool value_simple_{false};
std::function<bool(Ts...)> value_template_{};
union {
bool simple;
bool (*template_func)(Ts...);
} value_{.simple = false};
};
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {

View File

@@ -117,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
}
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
if (this->data_to_value_func_.has_value()) {
if (this->has_data_to_value_) {
std::vector<uint8_t> data(value, value + value_len);
return (*this->data_to_value_func_)(data);
return this->data_to_value_func_(data);
} else {
return value[0];
}

View File

@@ -15,8 +15,6 @@ namespace ble_client {
namespace espbt = esphome::esp32_ble_tracker;
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
public:
void loop() override;
@@ -33,13 +31,17 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
this->data_to_value_func_ = lambda;
this->has_data_to_value_ = true;
}
void set_enable_notify(bool notify) { this->notify_ = notify; }
uint16_t handle;
protected:
float parse_data_(uint8_t *value, uint16_t value_len);
optional<data_to_value_t> data_to_value_func_{};
bool has_data_to_value_{false};
float (*data_to_value_func_)(const std::vector<uint8_t> &){};
bool notify_;
espbt::ESPBTUUID service_uuid_;
espbt::ESPBTUUID char_uuid_;

View File

@@ -84,11 +84,6 @@ def button_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):
await setup_entity(var, config, "button")

View File

@@ -270,11 +270,6 @@ def climate_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):
await setup_entity(var, config, "climate")

View File

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

View File

@@ -151,11 +151,6 @@ def cover_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):
await setup_entity(var, config, "cover")

View File

@@ -76,10 +76,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
if isinstance(value, str):
value = list(value.encode(value_config[CONF_STRING_ENCODING]))
if isinstance(value, list):
return cg.std_vector.template(cg.uint8)(value)
# Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
# This calls the set_value(std::initializer_list<uint8_t>) overload
return cg.ArrayInitializer(*value)
val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])

View File

@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
xSemaphoreTake(this->set_value_lock_, 0L);
this->value_ = buffer;
this->value_ = std::move(buffer);
xSemaphoreGive(this->set_value_lock_);
}
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
}
void BLECharacteristic::set_value(const std::string &buffer) {
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end())); // Delegate to move overload
}
void BLECharacteristic::notify() {

View File

@@ -33,7 +33,8 @@ class BLECharacteristic {
~BLECharacteristic();
void set_value(ByteBuffer buffer);
void set_value(const std::vector<uint8_t> &buffer);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(const std::string &buffer);
void set_broadcast_property(bool value);

View File

@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
this->state_ = CREATING;
}
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
size_t length = buffer.size();
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
if (length > this->value_.attr_max_len) {
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
return;
}
this->value_.attr_len = length;
memcpy(this->value_.attr_value, buffer.data(), length);
memcpy(this->value_.attr_value, data, length);
}
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,

View File

@@ -27,7 +27,8 @@ class BLEDescriptor {
void do_create(BLECharacteristic *characteristic);
ESPBTUUID get_uuid() const { return this->uuid_; }
void set_value(std::vector<uint8_t> buffer);
void set_value(std::vector<uint8_t> &&buffer);
void set_value(std::initializer_list<uint8_t> data);
void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
@@ -42,6 +43,8 @@ class BLEDescriptor {
}
protected:
void set_value_impl_(const uint8_t *data, size_t length);
BLECharacteristic *characteristic_{nullptr};
ESPBTUUID uuid_;
uint16_t handle_{0xFFFF};

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
}
}
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
this->rpc_response_->set_value(ByteBuffer::wrap(response));
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
this->rpc_response_->set_value(std::move(response));
if (this->state_ != improv::STATE_STOPPED)
this->rpc_response_->notify();
}
@@ -409,10 +409,8 @@ void ESP32ImprovComponent::check_wifi_connection_() {
}
}
#endif
// Pass to build_rpc_response using vector constructor from iterators to avoid extra copies
std::vector<uint8_t> data = improv::build_rpc_response(
improv::WIFI_SETTINGS, std::vector<std::string>(url_strings, url_strings + url_count));
this->send_response_(data);
this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
std::vector<std::string>(url_strings, url_strings + url_count)));
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
ESP_LOGD(TAG, "WiFi provisioned externally");
}

View File

@@ -109,7 +109,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error);
improv::State get_initial_state_() const;
void send_response_(std::vector<uint8_t> &response);
void send_response_(std::vector<uint8_t> &&response);
void process_incoming_data_();
void on_wifi_connect_timeout_();
void check_wifi_connection_();

View File

@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
from esphome.components.network import IPAddress
from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
import esphome.config_validation as cv
from esphome.const import (
@@ -320,11 +320,11 @@ def _final_validate_spi(config):
def manual_ip(config):
return cg.StructInitializer(
ManualIP,
("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
("gateway", IPAddress(str(config[CONF_GATEWAY]))),
("subnet", IPAddress(str(config[CONF_SUBNET]))),
("dns1", IPAddress(str(config[CONF_DNS1]))),
("dns2", IPAddress(str(config[CONF_DNS2]))),
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
("gateway", ip_address_literal(config[CONF_GATEWAY])),
("subnet", ip_address_literal(config[CONF_SUBNET])),
("dns1", ip_address_literal(config[CONF_DNS1])),
("dns2", ip_address_literal(config[CONF_DNS2])),
)

View File

@@ -85,11 +85,6 @@ def event_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]):
await setup_entity(var, config, "event")

View File

@@ -189,10 +189,6 @@ def fan_schema(
return _FAN_SCHEMA.extend(schema)
# Remove before 2025.11.0
FAN_SCHEMA = fan_schema(Fan)
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
_PRESET_MODES_SCHEMA = cv.All(
cv.ensure_list(cv.string_strict),
cv.Length(min=1),

View File

@@ -51,14 +51,7 @@ void FanCall::validate_() {
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
bool found = false;
for (const auto &mode : preset_modes) {
if (mode == this->preset_mode_) {
found = true;
break;
}
}
if (!found) {
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.clear();
}
@@ -198,14 +191,9 @@ void Fan::save_state_() {
if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) {
const auto &preset_modes = this->get_traits().supported_preset_modes();
// Store index of current preset mode
size_t i = 0;
for (const auto &mode : preset_modes) {
if (mode == this->preset_mode) {
state.preset_mode = i;
break;
}
i++;
}
auto preset_iterator = preset_modes.find(this->preset_mode);
if (preset_iterator != preset_modes.end())
state.preset_mode = std::distance(preset_modes.begin(), preset_iterator);
}
this->rtc_.save(&state);

View File

@@ -60,8 +60,6 @@ class FanCall {
this->speed_ = speed;
return *this;
}
ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
FanCall &set_speed(const char *legacy_speed);
optional<int> get_speed() const { return this->speed_; }
FanCall &set_direction(FanDirection direction) {
this->direction_ = direction;

View File

@@ -1,6 +1,7 @@
#pragma once
#include <set>
#include <utility>
#include <vector>
#pragma once
namespace esphome {
@@ -35,9 +36,9 @@ class FanTraits {
/// Set whether this fan supports changing direction
void set_direction(bool direction) { this->direction_ = direction; }
/// Return the preset modes supported by the fan.
const std::vector<std::string> &supported_preset_modes() const { return this->preset_modes_; }
std::set<std::string> supported_preset_modes() const { return this->preset_modes_; }
/// Set the preset modes supported by the fan.
void set_supported_preset_modes(const std::vector<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
void set_supported_preset_modes(const std::set<std::string> &preset_modes) { this->preset_modes_ = preset_modes; }
/// Return if preset modes are supported
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
@@ -45,17 +46,17 @@ class FanTraits {
#ifdef USE_API
// The API connection is a friend class to access internal methods
friend class api::APIConnection;
// This method returns a reference to the internal preset modes.
// This method returns a reference to the internal preset modes set.
// It is used by the API to avoid copying data when encoding messages.
// Warning: Do not use this method outside of the API connection code.
// It returns a reference to internal data that can be invalidated.
const std::vector<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
#endif
bool oscillation_{false};
bool speed_{false};
bool direction_{false};
int speed_count_{};
std::vector<std::string> preset_modes_{};
std::set<std::string> preset_modes_{};
};
} // namespace fan

View File

@@ -22,7 +22,7 @@ class HBridgeFan : public Component, public fan::Fan {
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; }
void set_preset_modes(const std::vector<std::string> &presets) { preset_modes_ = presets; }
void set_preset_modes(const std::set<std::string> &presets) { preset_modes_ = presets; }
void setup() override;
void dump_config() override;
@@ -38,7 +38,7 @@ class HBridgeFan : public Component, public fan::Fan {
int speed_count_{};
DecayMode decay_mode_{DECAY_MODE_SLOW};
fan::FanTraits traits_;
std::vector<std::string> preset_modes_{};
std::set<std::string> preset_modes_{};
void control(const fan::FanCall &call) override;
void write_state_();

View File

@@ -12,7 +12,6 @@ from esphome.const import (
CONF_ON_ERROR,
CONF_ON_RESPONSE,
CONF_TIMEOUT,
CONF_TRIGGER_ID,
CONF_URL,
CONF_WATCHDOG_TIMEOUT,
PLATFORM_HOST,
@@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema(
f"{CONF_VERIFY_SSL} has moved to the base component configuration."
),
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)}
),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
automation.Trigger.template()
)
}
),
cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True),
cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes,
}
)
@@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
template_ = await cg.templatable(config[CONF_URL], args, cg.std_string)
cg.add(var.set_url(template_))
cg.add(var.set_method(config[CONF_METHOD]))
cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE]))
capture_response = config[CONF_CAPTURE_RESPONSE]
if capture_response:
cg.add(var.set_capture_response(capture_response))
cg.add_define("USE_HTTP_REQUEST_RESPONSE")
cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE]))
if CONF_BODY in config:
@@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args):
for value in config.get(CONF_COLLECT_HEADERS, []):
cg.add(var.add_collect_header(value))
for conf in config.get(CONF_ON_RESPONSE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
cg.add(var.register_response_trigger(trigger))
await automation.build_automation(
trigger,
[
(cg.std_shared_ptr.template(HttpContainer), "response"),
(cg.std_string_ref, "body"),
],
conf,
)
for conf in config.get(CONF_ON_ERROR, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID])
cg.add(var.register_error_trigger(trigger))
await automation.build_automation(trigger, [], conf)
if response_conf := config.get(CONF_ON_RESPONSE):
if capture_response:
await automation.build_automation(
var.get_success_trigger_with_response(),
[
(cg.std_shared_ptr.template(HttpContainer), "response"),
(cg.std_string_ref, "body"),
*args,
],
response_conf,
)
else:
await automation.build_automation(
var.get_success_trigger(),
[(cg.std_shared_ptr.template(HttpContainer), "response"), *args],
response_conf,
)
if error_conf := config.get(CONF_ON_ERROR):
await automation.build_automation(var.get_error_trigger(), args, error_conf)
return var

View File

@@ -124,7 +124,7 @@ class HttpRequestComponent : public Component {
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void set_useragent(const char *useragent) { this->useragent_ = useragent; }
void set_timeout(uint16_t timeout) { this->timeout_ = timeout; }
void set_timeout(uint32_t timeout) { this->timeout_ = timeout; }
void set_watchdog_timeout(uint32_t watchdog_timeout) { this->watchdog_timeout_ = watchdog_timeout; }
uint32_t get_watchdog_timeout() const { return this->watchdog_timeout_; }
void set_follow_redirects(bool follow_redirects) { this->follow_redirects_ = follow_redirects; }
@@ -173,7 +173,7 @@ class HttpRequestComponent : public Component {
const char *useragent_{nullptr};
bool follow_redirects_{};
uint16_t redirect_limit_{};
uint16_t timeout_{4500};
uint32_t timeout_{4500};
uint32_t watchdog_timeout_{0};
};
@@ -183,7 +183,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
TEMPLATABLE_VALUE(std::string, url)
TEMPLATABLE_VALUE(const char *, method)
TEMPLATABLE_VALUE(std::string, body)
#ifdef USE_HTTP_REQUEST_RESPONSE
TEMPLATABLE_VALUE(bool, capture_response)
#endif
void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) {
this->request_headers_.insert({key, value});
@@ -195,9 +197,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; }
void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); }
#ifdef USE_HTTP_REQUEST_RESPONSE
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const {
return this->success_trigger_with_response_;
}
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; }
void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); }
Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; }
void set_max_response_buffer_size(size_t max_response_buffer_size) {
this->max_response_buffer_size_ = max_response_buffer_size;
@@ -228,17 +235,20 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers,
this->collect_headers_);
auto captured_args = std::make_tuple(x...);
if (container == nullptr) {
for (auto *trigger : this->error_triggers_)
trigger->trigger();
std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); },
captured_args);
return;
}
size_t content_length = container->content_length;
size_t max_length = std::min(content_length, this->max_response_buffer_size_);
std::string response_body;
#ifdef USE_HTTP_REQUEST_RESPONSE
if (this->capture_response_.value(x...)) {
std::string response_body;
RAMAllocator<uint8_t> allocator;
uint8_t *buf = allocator.allocate(max_length);
if (buf != nullptr) {
@@ -253,19 +263,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
response_body.assign((char *) buf, read_index);
allocator.deallocate(buf, max_length);
}
}
if (this->response_triggers_.size() == 1) {
// if there is only one trigger, no need to copy the response body
this->response_triggers_[0]->process(container, response_body);
} else {
for (auto *trigger : this->response_triggers_) {
// with multiple triggers, pass a copy of the response body to each
// one so that modifications made in one trigger are not visible to
// the others
auto response_body_copy = std::string(response_body);
trigger->process(container, response_body_copy);
}
std::apply(
[this, &container, &response_body](Ts... captured_args_inner) {
this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...);
},
captured_args);
} else
#endif
{
std::apply([this, &container](
Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); },
captured_args);
}
container->end();
}
@@ -283,8 +291,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
std::set<std::string> collect_headers_{"content-type", "content-length"};
std::map<const char *, TemplatableValue<std::string, Ts...>> json_{};
std::function<void(Ts..., JsonObject)> json_func_{nullptr};
std::vector<HttpRequestResponseTrigger *> response_triggers_{};
std::vector<Trigger<> *> error_triggers_{};
#ifdef USE_HTTP_REQUEST_RESPONSE
Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ =
new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>();
#endif
Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ =
new Trigger<std::shared_ptr<HttpContainer>, Ts...>();
Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>();
size_t max_response_buffer_size_{SIZE_MAX};
};

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
case IMAGE_TYPE_RGB:
#if LV_COLOR_DEPTH == 32
switch (this->transparent_) {
switch (this->transparency_) {
case TRANSPARENCY_ALPHA_CHANNEL:
this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA;
break;
@@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() {
break;
}
#else
this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
this->dsc_.header.cf =
this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565;
#endif
break;
}

View File

@@ -57,9 +57,9 @@ class AddressableLightEffect : public LightEffect {
class AddressableLambdaLightEffect : public AddressableLightEffect {
public:
AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f,
AddressableLambdaLightEffect(const char *name, void (*f)(AddressableLight &, Color, bool initial_run),
uint32_t update_interval)
: AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
: AddressableLightEffect(name), f_(f), update_interval_(update_interval) {}
void start() override { this->initial_run_ = true; }
void apply(AddressableLight &it, const Color &current_color) override {
const uint32_t now = millis();
@@ -72,7 +72,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect {
}
protected:
std::function<void(AddressableLight &, Color, bool initial_run)> f_;
void (*f_)(AddressableLight &, Color, bool initial_run);
uint32_t update_interval_;
uint32_t last_run_{0};
bool initial_run_;

View File

@@ -112,8 +112,8 @@ class RandomLightEffect : public LightEffect {
class LambdaLightEffect : public LightEffect {
public:
LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval)
: LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {}
LambdaLightEffect(const char *name, void (*f)(bool initial_run), uint32_t update_interval)
: LightEffect(name), f_(f), update_interval_(update_interval) {}
void start() override { this->initial_run_ = true; }
void apply() override {
@@ -130,7 +130,7 @@ class LambdaLightEffect : public LightEffect {
uint32_t get_current_index() const { return this->get_index(); }
protected:
std::function<void(bool initial_run)> f_;
void (*f_)(bool initial_run);
uint32_t update_interval_;
uint32_t last_run_{0};
bool initial_run_;

View File

@@ -91,11 +91,6 @@ def lock_schema(
return _LOCK_SCHEMA.extend(schema)
# Remove before 2025.11.0
LOCK_SCHEMA = lock_schema()
LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock"))
async def _setup_lock_core(var, config):
await setup_entity(var, config, "lock")

View File

@@ -173,14 +173,34 @@ def uart_selection(value):
raise NotImplementedError
def validate_local_no_higher_than_global(value):
global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL])
for tag, level in value.get(CONF_LOGS, {}).items():
if LOG_LEVEL_SEVERITY.index(level) > global_level:
raise cv.Invalid(
f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}."
def validate_local_no_higher_than_global(config):
global_level = config[CONF_LEVEL]
global_level_index = LOG_LEVEL_SEVERITY.index(global_level)
errs = []
for tag, level in config.get(CONF_LOGS, {}).items():
if LOG_LEVEL_SEVERITY.index(level) > global_level_index:
errs.append(
cv.Invalid(
f"The configured log level for {tag} ({level}) must not be less severe than the global log level ({global_level})",
[CONF_LOGS, tag],
)
)
return value
if errs:
raise cv.MultipleInvalid(errs)
return config
def validate_initial_no_higher_than_global(config):
if initial_level := config.get(CONF_INITIAL_LEVEL):
global_level = config[CONF_LEVEL]
if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index(
global_level
):
raise cv.Invalid(
f"The initial log level ({initial_level}) must not be less severe than the global log level ({global_level})",
[CONF_INITIAL_LEVEL],
)
return config
Logger = logger_ns.class_("Logger", cg.Component)
@@ -263,6 +283,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
validate_local_no_higher_than_global,
validate_initial_no_higher_than_global,
)

View File

@@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i
"""
import logging
from typing import TYPE_CHECKING, Any
from esphome import codegen as cg, config_validation as cv
from esphome.const import CONF_ITEMS
@@ -12,6 +13,7 @@ from esphome.core import ID, Lambda
from esphome.cpp_generator import LambdaExpression, MockObj
from esphome.cpp_types import uint32
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from .helpers import requires_component
@@ -42,7 +44,13 @@ def static_cast(type, value):
def call_lambda(lamb: LambdaExpression):
expr = lamb.content.strip()
if expr.startswith("return") and expr.endswith(";"):
return expr[6:][:-1].strip()
return expr[6:-1].strip()
# If lambda has parameters, call it with those parameter names
# Parameter names come from hardcoded component code (like "x", "it", "event")
# not from user input, so they're safe to use directly
if lamb.parameters and lamb.parameters.parameters:
param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters)
return f"{lamb}({param_names})"
return f"{lamb}()"
@@ -65,10 +73,20 @@ class LValidator:
return cv.returning_lambda(value)
return self.validator(value)
async def process(self, value, args=()):
async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
if value is None:
return None
if isinstance(value, Lambda):
# Local import to avoid circular import
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
return cg.RawExpression(
call_lambda(
await cg.process_lambda(value, args, return_type=self.rtype)

View File

@@ -1,3 +1,5 @@
from typing import TYPE_CHECKING, Any
import esphome.codegen as cg
from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
@@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj
from esphome.cpp_types import ESPTime, int32, uint32
from esphome.helpers import cpp_string_escape
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import Expression, SafeExpType
from . import types as ty
from .defines import (
@@ -388,11 +391,23 @@ class TextValidator(LValidator):
return value
return super().__call__(value)
async def process(self, value, args=()):
async def process(
self, value: Any, args: list[tuple[SafeExpType, str]] | None = None
) -> Expression:
# Local import to avoid circular import at module level
from .lvcode import CodeContext, LambdaContext
if TYPE_CHECKING:
# CodeContext does not have get_automation_parameters
# so we need to assert the type here
assert isinstance(CodeContext.code_context, LambdaContext)
args = args or CodeContext.code_context.get_automation_parameters()
if isinstance(value, dict):
if format_str := value.get(CONF_FORMAT):
args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(args))
str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str)
return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()")
if time_format := value.get(CONF_TIME_FORMAT):

View File

@@ -164,6 +164,9 @@ class LambdaContext(CodeContext):
code_text.append(text)
return code_text
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
return self.parameters
async def __aenter__(self):
await super().__aenter__()
add_line_marks(self.where)
@@ -178,9 +181,8 @@ class LvContext(LambdaContext):
added_lambda_count = 0
def __init__(self, args=None):
self.args = args or LVGL_COMP_ARG
super().__init__(parameters=self.args)
def __init__(self):
super().__init__(parameters=LVGL_COMP_ARG)
async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb)
@@ -189,6 +191,11 @@ class LvContext(LambdaContext):
cg.add(expression)
return expression
def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]:
# When generating automations, we don't want the `lv_component` parameter to be passed
# to the lambda.
return []
def __call__(self, *args):
return self.add(*args)

View File

@@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET
from ..lvcode import (
API_EVENT,
EVENT_ARG,
LVGL_COMP_ARG,
UPDATE_EVENT,
LambdaContext,
LvContext,
@@ -30,7 +29,7 @@ async def to_code(config):
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as lamb:
lv_add(sensor.publish_state(widget.get_value()))
async with LvContext(LVGL_COMP_ARG):
async with LvContext():
lv_add(
lvgl_static.add_event_cb(
widget.obj,

View File

@@ -33,7 +33,7 @@ from ..lv_validation import (
pixels,
size,
)
from ..lvcode import LocalVariable, lv, lv_assign
from ..lvcode import LocalVariable, lv, lv_assign, lv_expr
from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema
from ..types import LvType, ObjUpdateAction, WidgetType
from . import Widget, get_widgets
@@ -70,15 +70,18 @@ class CanvasType(WidgetType):
width = config[CONF_WIDTH]
height = config[CONF_HEIGHT]
use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else ""
lv.canvas_set_buffer(
w.obj,
lv.custom_mem_alloc(
literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})")
),
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
buf_size = literal(
f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})"
)
with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf:
cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});"))
lv.canvas_set_buffer(
w.obj,
buf,
width,
height,
literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"),
)
canvas_spec = CanvasType()

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -192,10 +192,6 @@ def media_player_schema(
return _MEDIA_PLAYER_SCHEMA.extend(schema)
# Remove before 2025.11.0
MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer)
MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player"))
MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id(
cv.Schema(
{

View File

@@ -384,6 +384,18 @@ class DriverChip:
transform[CONF_TRANSFORM] = True
return transform
def swap_xy_schema(self):
uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def add_madctl(self, sequence: list, config: dict):
# Add the MADCTL command to the sequence based on the configuration.
use_flip = config.get(CONF_USE_AXIS_FLIPS)

View File

@@ -46,6 +46,7 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_DC_PIN,
CONF_DIMENSIONS,
CONF_DISABLED,
CONF_ENABLE_PIN,
CONF_GREEN,
CONF_HSYNC_PIN,
@@ -117,16 +118,16 @@ def data_pin_set(length):
def model_schema(config):
model = MODELS[config[CONF_MODEL].upper()]
if transforms := model.transforms:
transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms})
for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y):
if x not in transforms:
transform = transform.extend(
{cv.Optional(x): cv.invalid(f"{x} not supported by this model")}
)
else:
transform = cv.invalid("This model does not support transforms")
transform = cv.Any(
cv.Schema(
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),
)
# RPI model does not use an init sequence, indicates with empty list
if model.initsequence is None:
# Custom model requires an init sequence
@@ -135,12 +136,16 @@ def model_schema(config):
else:
iseqconf = cv.Optional(CONF_INIT_SEQUENCE)
uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0
swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False)
# Dimensions are optional if the model has a default width and the swap_xy transform is not overridden
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required
# Dimensions are optional if the model has a default width and the x-y transform is not overridden
transform_config = config.get(CONF_TRANSFORM, {})
is_swapped = (
isinstance(transform_config, dict)
and transform_config.get(CONF_SWAP_XY, False) is True
)
cv_dimensions = (
cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required
)
pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18")
schema = display.FULL_DISPLAY_SCHEMA.extend(
{
@@ -157,7 +162,7 @@ def model_schema(config):
model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of(
*pixel_modes, lower=True
),
model.option(CONF_TRANSFORM, cv.UNDEFINED): transform,
cv.Optional(CONF_TRANSFORM): transform,
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
model.option(CONF_INVERT_COLORS, False): cv.boolean,
model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean,
@@ -270,7 +275,6 @@ async def to_code(config):
cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH]))
cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED]))
cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY]))
index = 0
dpins = []
if CONF_RED in config[CONF_DATA_PINS]:
red_pins = config[CONF_DATA_PINS][CONF_RED]

View File

@@ -131,19 +131,6 @@ def denominator(config):
) from StopIteration
def swap_xy_schema(model):
uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED
def validator(value):
if value:
raise cv.Invalid("Axis swapping not supported by this model")
return cv.boolean(value)
if uses_swap:
return {cv.Required(CONF_SWAP_XY): cv.boolean}
return {cv.Optional(CONF_SWAP_XY, default=False): validator}
def model_schema(config):
model = MODELS[config[CONF_MODEL]]
bus_mode = config[CONF_BUS_MODE]
@@ -152,7 +139,7 @@ def model_schema(config):
{
cv.Required(CONF_MIRROR_X): cv.boolean,
cv.Required(CONF_MIRROR_Y): cv.boolean,
**swap_xy_schema(model),
**model.swap_xy_schema(),
}
),
cv.one_of(CONF_DISABLED, lower=True),

View File

@@ -33,8 +33,8 @@ class ModbusBinarySensor : public Component, public binary_sensor::BinarySensor,
void dump_config() override;
using transform_func_t = std::function<optional<bool>(ModbusBinarySensor *, bool, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
using transform_func_t = optional<bool> (*)(ModbusBinarySensor *, bool, const std::vector<uint8_t> &);
void set_template(transform_func_t f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};

View File

@@ -31,10 +31,10 @@ class ModbusNumber : public number::Number, public Component, public SensorItem
void set_parent(ModbusController *parent) { this->parent_ = parent; }
void set_write_multiply(float factor) { this->multiply_by_ = factor; }
using transform_func_t = std::function<optional<float>(ModbusNumber *, float, const std::vector<uint8_t> &)>;
using write_transform_func_t = std::function<optional<float>(ModbusNumber *, float, std::vector<uint16_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
using transform_func_t = optional<float> (*)(ModbusNumber *, float, const std::vector<uint8_t> &);
using write_transform_func_t = optional<float> (*)(ModbusNumber *, float, std::vector<uint16_t> &);
void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:

View File

@@ -29,8 +29,8 @@ class ModbusFloatOutput : public output::FloatOutput, public Component, public S
// Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<float>(ModbusFloatOutput *, float, std::vector<uint16_t> &)>;
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
using write_transform_func_t = optional<float> (*)(ModbusFloatOutput *, float, std::vector<uint16_t> &);
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
@@ -60,8 +60,8 @@ class ModbusBinaryOutput : public output::BinaryOutput, public Component, public
// Do nothing
void parse_and_publish(const std::vector<uint8_t> &data) override{};
using write_transform_func_t = std::function<optional<bool>(ModbusBinaryOutput *, bool, std::vector<uint8_t> &)>;
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
using write_transform_func_t = optional<bool> (*)(ModbusBinaryOutput *, bool, std::vector<uint8_t> &);
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:

View File

@@ -28,7 +28,7 @@ void ModbusSelect::parse_and_publish(const std::vector<uint8_t> &data) {
if (map_it != this->mapping_.cend()) {
size_t idx = std::distance(this->mapping_.cbegin(), map_it);
new_state = std::string(this->traits.get_options()[idx]);
new_state = std::string(this->option_at(idx));
ESP_LOGV(TAG, "Found option %s for value %lld", new_state->c_str(), value);
} else {
ESP_LOGE(TAG, "No option found for mapping %lld", value);

View File

@@ -26,16 +26,15 @@ class ModbusSelect : public Component, public select::Select, public SensorItem
this->mapping_ = std::move(mapping);
}
using transform_func_t =
std::function<optional<std::string>(ModbusSelect *const, int64_t, const std::vector<uint8_t> &)>;
using write_transform_func_t =
std::function<optional<int64_t>(ModbusSelect *const, const std::string &, int64_t, std::vector<uint16_t> &)>;
using transform_func_t = optional<std::string> (*)(ModbusSelect *const, int64_t, const std::vector<uint8_t> &);
using write_transform_func_t = optional<int64_t> (*)(ModbusSelect *const, const std::string &, int64_t,
std::vector<uint16_t> &);
void set_parent(ModbusController *const parent) { this->parent_ = parent; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; }
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
void set_template(transform_func_t f) { this->transform_func_ = f; }
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override;

View File

@@ -25,9 +25,9 @@ class ModbusSensor : public Component, public sensor::Sensor, public SensorItem
void parse_and_publish(const std::vector<uint8_t> &data) override;
void dump_config() override;
using transform_func_t = std::function<optional<float>(ModbusSensor *, float, const std::vector<uint8_t> &)>;
using transform_func_t = optional<float> (*)(ModbusSensor *, float, const std::vector<uint8_t> &);
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
void set_template(transform_func_t f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};

View File

@@ -34,10 +34,10 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
void parse_and_publish(const std::vector<uint8_t> &data) override;
void set_parent(ModbusController *parent) { this->parent_ = parent; }
using transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, const std::vector<uint8_t> &)>;
using write_transform_func_t = std::function<optional<bool>(ModbusSwitch *, bool, std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->publish_transform_func_ = f; }
void set_write_template(write_transform_func_t &&f) { this->write_transform_func_ = f; }
using transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, const std::vector<uint8_t> &);
using write_transform_func_t = optional<bool> (*)(ModbusSwitch *, bool, std::vector<uint8_t> &);
void set_template(transform_func_t f) { this->publish_transform_func_ = f; }
void set_write_template(write_transform_func_t f) { this->write_transform_func_ = f; }
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:

View File

@@ -30,9 +30,8 @@ class ModbusTextSensor : public Component, public text_sensor::TextSensor, publi
void dump_config() override;
void parse_and_publish(const std::vector<uint8_t> &data) override;
using transform_func_t =
std::function<optional<std::string>(ModbusTextSensor *, std::string, const std::vector<uint8_t> &)>;
void set_template(transform_func_t &&f) { this->transform_func_ = f; }
using transform_func_t = optional<std::string> (*)(ModbusTextSensor *, std::string, const std::vector<uint8_t> &);
void set_template(transform_func_t f) { this->transform_func_ = f; }
protected:
optional<transform_func_t> transform_func_{nullopt};

View File

@@ -1,3 +1,5 @@
import ipaddress
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_sdkconfig_option
import esphome.config_validation as cv
@@ -10,6 +12,41 @@ AUTO_LOAD = ["mdns"]
network_ns = cg.esphome_ns.namespace("network")
IPAddress = network_ns.class_("IPAddress")
def ip_address_literal(ip: str | int | None) -> cg.MockObj:
"""Generate an IPAddress with compile-time initialization instead of runtime parsing.
This function parses the IP address in Python during code generation and generates
a call to the 4-octet constructor (IPAddress(192, 168, 1, 1)) instead of the
string constructor (IPAddress("192.168.1.1")). This eliminates runtime string
parsing overhead and reduces flash usage on embedded systems.
Args:
ip: IP address as string (e.g., "192.168.1.1"), ipaddress.IPv4Address, or None
Returns:
IPAddress expression that uses 4-octet constructor for efficiency
"""
if ip is None:
return IPAddress(0, 0, 0, 0)
try:
# Parse using Python's ipaddress module
ip_obj = ipaddress.ip_address(ip)
except (ValueError, TypeError):
pass
else:
# Only support IPv4 for now
if isinstance(ip_obj, ipaddress.IPv4Address):
# Extract octets from the packed bytes representation
octets = ip_obj.packed
# Generate call to 4-octet constructor: IPAddress(192, 168, 1, 1)
return IPAddress(octets[0], octets[1], octets[2], octets[3])
# Fallback to string constructor if parsing fails
return IPAddress(str(ip))
CONFIG_SCHEMA = cv.Schema(
{
cv.SplitDefault(

View File

@@ -540,6 +540,23 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
*/
void goto_page(uint8_t page);
/**
* Set the visibility of a component.
*
* @param component The component name.
* @param show True to show the component, false to hide it.
*
* @see show_component()
* @see hide_component()
*
* Example:
* ```cpp
* it.set_component_visibility("textview", true); // Equivalent to show_component("textview")
* it.set_component_visibility("textview", false); // Equivalent to hide_component("textview")
* ```
*/
void set_component_visibility(const char *component, bool show) override;
/**
* Hide a component.
* @param component The component name.

View File

@@ -45,6 +45,7 @@ class NextionBase {
virtual void set_component_pressed_font_color(const char *component, Color color) = 0;
virtual void set_component_font(const char *component, uint8_t font_id) = 0;
virtual void set_component_visibility(const char *component, bool show) = 0;
virtual void show_component(const char *component) = 0;
virtual void hide_component(const char *component) = 0;

View File

@@ -201,13 +201,13 @@ void Nextion::set_component_font(const char *component, uint8_t font_id) {
this->add_no_result_to_queue_with_printf_("set_component_font", "%s.font=%" PRIu8, component, font_id);
}
void Nextion::hide_component(const char *component) {
this->add_no_result_to_queue_with_printf_("hide_component", "vis %s,0", component);
void Nextion::set_component_visibility(const char *component, bool show) {
this->add_no_result_to_queue_with_printf_("set_component_visibility", "vis %s,%d", component, show ? 1 : 0);
}
void Nextion::show_component(const char *component) {
this->add_no_result_to_queue_with_printf_("show_component", "vis %s,1", component);
}
void Nextion::hide_component(const char *component) { this->set_component_visibility(component, false); }
void Nextion::show_component(const char *component) { this->set_component_visibility(component, true); }
void Nextion::enable_component_touch(const char *component) {
this->add_no_result_to_queue_with_printf_("enable_component_touch", "tsw %s,1", component);

View File

@@ -81,13 +81,11 @@ void NextionComponent::update_component_settings(bool force_update) {
this->component_flags_.visible_needs_update = false;
if (this->component_flags_.visible) {
this->nextion_->show_component(name_to_send.c_str());
this->send_state_to_nextion();
} else {
this->nextion_->hide_component(name_to_send.c_str());
this->nextion_->set_component_visibility(name_to_send.c_str(), this->component_flags_.visible);
if (!this->component_flags_.visible) {
return;
}
this->send_state_to_nextion();
}
if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {

View File

@@ -174,11 +174,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate();
static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
115200, 230400, 250000, 256000, 512000, 921600};
if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client

View File

@@ -177,11 +177,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
// Check if baud rate is supported
this->original_baud_rate_ = this->parent_->get_baud_rate();
static const std::vector<uint32_t> SUPPORTED_BAUD_RATES = {2400, 4800, 9600, 19200, 31250, 38400, 57600,
115200, 230400, 250000, 256000, 512000, 921600};
if (std::find(SUPPORTED_BAUD_RATES.begin(), SUPPORTED_BAUD_RATES.end(), baud_rate) == SUPPORTED_BAUD_RATES.end()) {
baud_rate = this->original_baud_rate_;
}
ESP_LOGD(TAG, "Baud rate: %" PRIu32, baud_rate);
// Define the configuration for the HTTP client

View File

@@ -238,11 +238,6 @@ def number_schema(
return _NUMBER_SCHEMA.extend(schema)
# Remove before 2025.11.0
NUMBER_SCHEMA = number_schema(Number)
NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number"))
async def setup_number_core_(
var, config, *, min_value: float, max_value: float, step: float
):

View File

@@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.Any(
str: PACKAGE_SCHEMA,
}
),
cv.ensure_list(PACKAGE_SCHEMA),
[PACKAGE_SCHEMA],
)

View File

@@ -17,6 +17,7 @@ from esphome.const import (
CONF_FAMILY,
CONF_GROUP,
CONF_ID,
CONF_INDEX,
CONF_INVERTED,
CONF_LEVEL,
CONF_MAGNITUDE,
@@ -616,6 +617,49 @@ async def dooya_action(var, config, args):
cg.add(var.set_check(template_))
# Dyson
DysonData, DysonBinarySensor, DysonTrigger, DysonAction, DysonDumper = declare_protocol(
"Dyson"
)
DYSON_SCHEMA = cv.Schema(
{
cv.Required(CONF_CODE): cv.hex_uint16_t,
cv.Optional(CONF_INDEX, default=0xFF): cv.hex_uint8_t,
}
)
@register_binary_sensor("dyson", DysonBinarySensor, DYSON_SCHEMA)
def dyson_binary_sensor(var, config):
cg.add(
var.set_data(
cg.StructInitializer(
DysonData,
("code", config[CONF_CODE]),
("index", config[CONF_INDEX]),
)
)
)
@register_trigger("dyson", DysonTrigger, DysonData)
def dyson_trigger(var, config):
pass
@register_dumper("dyson", DysonDumper)
def dyson_dumper(var, config):
pass
@register_action("dyson", DysonAction, DYSON_SCHEMA)
async def dyson_action(var, config, args):
template_ = await cg.templatable(config[CONF_CODE], args, cg.uint16)
cg.add(var.set_code(template_))
template_ = await cg.templatable(config[CONF_INDEX], args, cg.uint8)
cg.add(var.set_index(template_))
# JVC
JVCData, JVCBinarySensor, JVCTrigger, JVCAction, JVCDumper = declare_protocol("JVC")
JVC_SCHEMA = cv.Schema({cv.Required(CONF_DATA): cv.hex_uint32_t})

View File

@@ -0,0 +1,71 @@
#include "dyson_protocol.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
static const char *const TAG = "remote.dyson";
// pulsewidth [µs]
constexpr uint32_t PW_MARK_US = 780;
constexpr uint32_t PW_SHORT_US = 720;
constexpr uint32_t PW_LONG_US = 1500;
constexpr uint32_t PW_START_US = 2280;
// MSB of 15 bit dyson code
constexpr uint16_t MSB_DYSON = (1 << 14);
// required symbols in transmit buffer = (start_symbol + 15 data_symbols)
constexpr uint32_t N_SYMBOLS_REQ = 2u * (1 + 15);
void DysonProtocol::encode(RemoteTransmitData *dst, const DysonData &data) {
uint32_t raw_code = (data.code << 2) + (data.index & 3);
dst->set_carrier_frequency(36000);
dst->reserve(N_SYMBOLS_REQ + 1);
dst->item(PW_START_US, PW_SHORT_US);
for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) {
if (mask == (mask & raw_code)) {
dst->item(PW_MARK_US, PW_LONG_US);
} else {
dst->item(PW_MARK_US, PW_SHORT_US);
}
}
dst->mark(PW_MARK_US); // final carrier pulse
}
optional<DysonData> DysonProtocol::decode(RemoteReceiveData src) {
uint32_t n_received = static_cast<uint32_t>(src.size());
uint16_t raw_code = 0;
DysonData data{
.code = 0,
.index = 0,
};
if (n_received < N_SYMBOLS_REQ)
return {}; // invalid frame length
if (!src.expect_item(PW_START_US, PW_SHORT_US))
return {}; // start not found
for (uint16_t mask = MSB_DYSON; mask != 0; mask >>= 1) {
if (src.expect_item(PW_MARK_US, PW_SHORT_US)) {
raw_code &= ~mask; // zero detected
} else if (src.expect_item(PW_MARK_US, PW_LONG_US)) {
raw_code |= mask; // one detected
} else {
return {}; // invalid data item
}
}
data.code = raw_code >> 2; // extract button code
data.index = raw_code & 3; // extract rolling index
if (src.expect_mark(PW_MARK_US)) { // check total length
return data;
}
return {}; // frame not complete
}
void DysonProtocol::dump(const DysonData &data) {
ESP_LOGI(TAG, "Dyson: code=0x%x rolling index=%d", data.code, data.index);
}
} // namespace remote_base
} // namespace esphome

View File

@@ -0,0 +1,46 @@
#pragma once
#include "remote_base.h"
#include <cinttypes>
namespace esphome {
namespace remote_base {
static constexpr uint8_t IGNORE_INDEX = 0xFF;
struct DysonData {
uint16_t code; // the button, e.g. power, swing, fan++, ...
uint8_t index; // the rolling index counter
bool operator==(const DysonData &rhs) const {
if (IGNORE_INDEX == index || IGNORE_INDEX == rhs.index) {
return code == rhs.code;
}
return code == rhs.code && index == rhs.index;
}
};
class DysonProtocol : public RemoteProtocol<DysonData> {
public:
void encode(RemoteTransmitData *dst, const DysonData &data) override;
optional<DysonData> decode(RemoteReceiveData src) override;
void dump(const DysonData &data) override;
};
DECLARE_REMOTE_PROTOCOL(Dyson)
template<typename... Ts> class DysonAction : public RemoteTransmitterActionBase<Ts...> {
public:
TEMPLATABLE_VALUE(uint16_t, code)
TEMPLATABLE_VALUE(uint8_t, index)
void encode(RemoteTransmitData *dst, Ts... x) override {
DysonData data{};
data.code = this->code_.value(x...);
data.index = this->index_.value(x...);
DysonProtocol().encode(dst, data);
}
};
} // namespace remote_base
} // namespace esphome

View File

@@ -1,3 +1,5 @@
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import esp32, esp32_rmt, remote_base
@@ -18,9 +20,12 @@ from esphome.const import (
)
from esphome.core import CORE
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["remote_base"]
CONF_EOT_LEVEL = "eot_level"
CONF_NON_BLOCKING = "non_blocking"
CONF_ON_TRANSMIT = "on_transmit"
CONF_ON_COMPLETE = "on_complete"
CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID
@@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema(
esp32_c6=48,
esp32_h2=48,
): cv.All(cv.only_on_esp32, cv.int_range(min=2)),
cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean),
cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA)
def _validate_non_blocking(config):
if CORE.is_esp32 and CONF_NON_BLOCKING not in config:
_LOGGER.warning(
"'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n"
"The default behavior changed in 2025.11.0; previously blocking mode was used.\n"
"To silence this warning, explicitly set 'non_blocking: true' (or 'false')."
)
config[CONF_NON_BLOCKING] = True
FINAL_VALIDATE_SCHEMA = _validate_non_blocking
DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent),
@@ -95,6 +114,7 @@ async def to_code(config):
if CORE.is_esp32:
var = cg.new_Pvariable(config[CONF_ID], pin)
cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS]))
cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING]))
if CONF_CLOCK_RESOLUTION in config:
cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION]))
if CONF_USE_DMA in config:

View File

@@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#if defined(USE_ESP32)
void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; }
void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; }
void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; }
#endif
Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; };
@@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
#ifdef USE_ESP32
void configure_rmt_();
void wait_for_rmt_();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
RemoteTransmitterComponentStore store_{};
@@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase,
esp_err_t error_code_{ESP_OK};
std::string error_string_{""};
bool inverted_{false};
bool non_blocking_{false};
#endif
uint8_t carrier_duty_percent_;

View File

@@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() {
}
}
void RemoteTransmitterComponent::wait_for_rmt_() {
esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
}
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1)
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {
uint64_t total_duration = 0;
if (this->is_failed()) {
return;
}
// if the timeout was cancelled, block until the tx is complete
if (this->non_blocking_ && this->cancel_timeout("complete")) {
this->wait_for_rmt_();
}
if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) {
this->current_carrier_frequency_ = this->temp_.get_carrier_frequency();
this->configure_rmt_();
@@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
// encode any delay at the start of the buffer to simplify the encoder callback
// this will be skipped the first time around
total_duration += send_wait * (send_times - 1);
send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait));
while (send_wait > 0) {
int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX));
@@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
if (!level) {
value = -value;
}
total_duration += value * send_times;
value = this->from_microseconds_(static_cast<uint32_t>(value));
while (value > 0) {
int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX));
@@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen
} else {
this->status_clear_warning();
}
error = rmt_tx_wait_all_done(this->channel_, -1);
if (error != ESP_OK) {
ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error));
this->status_set_warning();
}
this->complete_trigger_->trigger();
if (this->non_blocking_) {
this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); });
} else {
this->wait_for_rmt_();
}
}
#else
void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) {

View File

@@ -86,11 +86,6 @@ def select_schema(
return _SELECT_SCHEMA.extend(schema)
# Remove before 2025.11.0
SELECT_SCHEMA = select_schema(Select)
SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select"))
async def setup_select_core_(var, config, *, options: list[str]):
await setup_entity(var, config, "select")

View File

@@ -60,5 +60,7 @@ optional<std::string> Select::at(size_t index) const {
}
}
const char *Select::option_at(size_t index) const { return traits.get_options().at(index); }
} // namespace select
} // namespace esphome

View File

@@ -56,6 +56,9 @@ class Select : public EntityBase {
/// Return the (optional) option value at the provided index offset.
optional<std::string> at(size_t index) const;
/// Return the option value at the provided index offset (as const char* from flash).
const char *option_at(size_t index) const;
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
protected:

View File

@@ -369,11 +369,6 @@ def sensor_schema(
return _SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
SENSOR_SCHEMA = sensor_schema()
SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor"))
@FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_))
async def offset_filter_to_code(config, filter_id):
template_ = await cg.templatable(config, [], float)

View File

@@ -18,7 +18,7 @@ class SpeedFan : public Component, public fan::Fan {
void set_output(output::FloatOutput *output) { this->output_ = output; }
void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; }
void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; }
void set_preset_modes(const std::vector<std::string> &presets) { this->preset_modes_ = presets; }
void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; }
fan::FanTraits get_traits() override { return this->traits_; }
protected:
@@ -30,7 +30,7 @@ class SpeedFan : public Component, public fan::Fan {
output::BinaryOutput *direction_{nullptr};
int speed_count_{};
fan::FanTraits traits_;
std::vector<std::string> preset_modes_{};
std::set<std::string> preset_modes_{};
};
} // namespace speed

View File

@@ -1,4 +1,6 @@
import logging
from re import Match
from typing import Any
from esphome import core
from esphome.config_helpers import Extend, Remove, merge_config, merge_dicts_ordered
@@ -39,7 +41,34 @@ async def to_code(config):
pass
def _expand_jinja(value, orig_value, path, jinja, ignore_missing):
def _restore_data_base(value: Any, orig_value: ESPHomeDataBase) -> ESPHomeDataBase:
"""This function restores ESPHomeDataBase metadata held by the original string.
This is needed because during jinja evaluation, strings can be replaced by other types,
but we want to keep the original metadata for error reporting and source mapping.
For example, if a substitution replaces a string with a dictionary, we want that items
in the dictionary to still point to the original document location
"""
if isinstance(value, ESPHomeDataBase):
return value
if isinstance(value, dict):
return {
_restore_data_base(k, orig_value): _restore_data_base(v, orig_value)
for k, v in value.items()
}
if isinstance(value, list):
return [_restore_data_base(v, orig_value) for v in value]
if isinstance(value, str):
return make_data_base(value, orig_value)
return value
def _expand_jinja(
value: str | JinjaStr,
orig_value: str | JinjaStr,
path,
jinja: Jinja,
ignore_missing: bool,
) -> Any:
if has_jinja(value):
# If the original value passed in to this function is a JinjaStr, it means it contains an unresolved
# Jinja expression from a previous pass.
@@ -65,10 +94,17 @@ def _expand_jinja(value, orig_value, path, jinja, ignore_missing):
f"\nSee {'->'.join(str(x) for x in path)}",
path,
)
# If the original, unexpanded string, contained document metadata (ESPHomeDatabase),
# assign this same document metadata to the resulting value.
if isinstance(orig_value, ESPHomeDataBase):
value = _restore_data_base(value, orig_value)
return value
def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
def _expand_substitutions(
substitutions: dict, value: str, path, jinja: Jinja, ignore_missing: bool
) -> Any:
if "$" not in value:
return value
@@ -76,14 +112,14 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
i = 0
while True:
m = cv.VARIABLE_PROG.search(value, i)
m: Match[str] = cv.VARIABLE_PROG.search(value, i)
if not m:
# No more variable substitutions found. See if the remainder looks like a jinja template
value = _expand_jinja(value, orig_value, path, jinja, ignore_missing)
break
i, j = m.span(0)
name = m.group(1)
name: str = m.group(1)
if name.startswith("{") and name.endswith("}"):
name = name[1:-1]
if name not in substitutions:
@@ -98,7 +134,7 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
i = j
continue
sub = substitutions[name]
sub: Any = substitutions[name]
if i == 0 and j == len(value):
# The variable spans the whole expression, e.g., "${varName}". Return its resolved value directly
@@ -121,7 +157,13 @@ def _expand_substitutions(substitutions, value, path, jinja, ignore_missing):
return value
def _substitute_item(substitutions, item, path, jinja, ignore_missing):
def _substitute_item(
substitutions: dict,
item: Any,
path: list[int | str],
jinja: Jinja,
ignore_missing: bool,
) -> Any | None:
if isinstance(item, ESPLiteralValue):
return None # do not substitute inside literal blocks
if isinstance(item, list):
@@ -160,7 +202,9 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing):
return None
def do_substitution_pass(config, command_line_substitutions, ignore_missing=False):
def do_substitution_pass(
config: dict, command_line_substitutions: dict, ignore_missing: bool = False
) -> None:
if CONF_SUBSTITUTIONS not in config and not command_line_substitutions:
return

View File

@@ -1,10 +1,14 @@
from ast import literal_eval
from collections.abc import Iterator
from itertools import chain, islice
import logging
import math
import re
from types import GeneratorType
from typing import Any
import jinja2 as jinja
from jinja2.sandbox import SandboxedEnvironment
from jinja2.nativetypes import NativeCodeGenerator, NativeTemplate
from esphome.yaml_util import ESPLiteralValue
@@ -24,7 +28,7 @@ detect_jinja_re = re.compile(
)
def has_jinja(st):
def has_jinja(st: str) -> bool:
return detect_jinja_re.search(st) is not None
@@ -109,12 +113,56 @@ class TrackerContext(jinja.runtime.Context):
return val
class Jinja(SandboxedEnvironment):
def _concat_nodes_override(values: Iterator[Any]) -> Any:
"""
This function customizes how Jinja preserves native types when concatenating
multiple result nodes together. If the result is a single node, its value
is returned. Otherwise, the nodes are concatenated as strings. If
the result can be parsed with `ast.literal_eval`, the parsed
value is returned. Otherwise, the string is returned.
This helps preserve metadata such as ESPHomeDataBase from original values
and mimicks how HomeAssistant deals with template evaluation and preserving
the original datatype.
"""
head: list[Any] = list(islice(values, 2))
if not head:
return None
if len(head) == 1:
raw = head[0]
if not isinstance(raw, str):
return raw
else:
if isinstance(values, GeneratorType):
values = chain(head, values)
raw = "".join([str(v) for v in values])
try:
# Attempt to parse the concatenated string into a Python literal.
# This allows expressions like "1 + 2" to be evaluated to the integer 3.
# If the result is also a string or there is a parsing error,
# fall back to returning the raw string. This is consistent with
# Home Assistant's behavior when evaluating templates
result = literal_eval(raw)
if not isinstance(result, str):
return result
except (ValueError, SyntaxError, MemoryError, TypeError):
pass
return raw
class Jinja(jinja.Environment):
"""
Wraps a Jinja environment
"""
def __init__(self, context_vars):
# jinja environment customization overrides
code_generator_class = NativeCodeGenerator
concat = staticmethod(_concat_nodes_override)
def __init__(self, context_vars: dict):
super().__init__(
trim_blocks=True,
lstrip_blocks=True,
@@ -142,19 +190,10 @@ class Jinja(SandboxedEnvironment):
**SAFE_GLOBALS,
}
def safe_eval(self, expr):
try:
result = literal_eval(expr)
if not isinstance(result, str):
return result
except (ValueError, SyntaxError, MemoryError, TypeError):
pass
return expr
def expand(self, content_str):
def expand(self, content_str: str | JinjaStr) -> Any:
"""
Renders a string that may contain Jinja expressions or statements
Returns the resulting processed string if all values could be resolved.
Returns the resulting value if all variables and expressions could be resolved.
Otherwise, it returns a tagged (JinjaStr) string that captures variables
in scope (upvalues), like a closure for later evaluation.
"""
@@ -172,7 +211,7 @@ class Jinja(SandboxedEnvironment):
self.context_trace = {}
try:
template = self.from_string(content_str)
result = self.safe_eval(template.render(override_vars))
result = template.render(override_vars)
if isinstance(result, Undefined):
print("" + result) # force a UndefinedError exception
except (TemplateSyntaxError, UndefinedError) as err:
@@ -201,3 +240,10 @@ class Jinja(SandboxedEnvironment):
content_str.result = result
return result, None
class JinjaTemplate(NativeTemplate):
environment_class = Jinja
Jinja.template_class = JinjaTemplate

View File

@@ -139,11 +139,6 @@ def switch_schema(
return _SWITCH_SCHEMA.extend(schema)
# Remove before 2025.11.0
SWITCH_SCHEMA = switch_schema(Switch)
SWITCH_SCHEMA.add_extra(cv.deprecated_schema_constant("switch"))
async def setup_switch_core_(var, config):
await setup_entity(var, config, "switch")

View File

@@ -25,6 +25,20 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
this->sensor_data_.push_back(sd);
this->sensor_map_[sensor].store_index = this->next_store_index_++;
};
static const LogString *sensor_type_to_string(AlarmSensorType type) {
switch (type) {
case ALARM_SENSOR_TYPE_INSTANT:
return LOG_STR("instant");
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
return LOG_STR("delayed_follower");
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
return LOG_STR("instant_always");
case ALARM_SENSOR_TYPE_DELAYED:
default:
return LOG_STR("delayed");
}
}
#endif
void TemplateAlarmControlPanel::dump_config() {
@@ -46,53 +60,31 @@ void TemplateAlarmControlPanel::dump_config() {
" Supported Features: %" PRIu32,
(this->pending_time_ / 1000), (this->trigger_time_ / 1000), this->get_supported_features());
#ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) {
ESP_LOGCONFIG(TAG, " Binary Sensor:");
for (auto const &[sensor, info] : this->sensor_map_) {
ESP_LOGCONFIG(TAG,
" Binary Sensor:\n"
" Name: %s\n"
" Type: %s\n"
" Armed home bypass: %s\n"
" Armed night bypass: %s\n"
" Auto bypass: %s\n"
" Chime mode: %s",
sensor_info.first->get_name().c_str(),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME));
const char *sensor_type;
switch (sensor_info.second.type) {
case ALARM_SENSOR_TYPE_INSTANT:
sensor_type = "instant";
break;
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
sensor_type = "delayed_follower";
break;
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
sensor_type = "instant_always";
break;
case ALARM_SENSOR_TYPE_DELAYED:
default:
sensor_type = "delayed";
}
ESP_LOGCONFIG(TAG, " Sensor type: %s", sensor_type);
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
}
#endif
}
void TemplateAlarmControlPanel::setup() {
switch (this->restore_mode_) {
case ALARM_CONTROL_PANEL_ALWAYS_DISARMED:
this->current_state_ = ACP_STATE_DISARMED;
break;
case ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED: {
uint8_t value;
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
} else {
this->current_state_ = ACP_STATE_DISARMED;
}
break;
this->current_state_ = ACP_STATE_DISARMED;
if (this->restore_mode_ == ALARM_CONTROL_PANEL_RESTORE_DEFAULT_DISARMED) {
uint8_t value;
this->pref_ = global_preferences->make_preference<uint8_t>(this->get_preference_hash());
if (this->pref_.load(&value)) {
this->current_state_ = static_cast<alarm_control_panel::AlarmControlPanelState>(value);
}
}
this->desired_state_ = this->current_state_;
@@ -119,86 +111,83 @@ void TemplateAlarmControlPanel::loop() {
this->publish_state(ACP_STATE_TRIGGERED);
return;
}
auto future_state = this->current_state_;
auto next_state = this->current_state_;
// reset triggered if all clear
if (this->current_state_ == ACP_STATE_TRIGGERED && this->trigger_time_ > 0 &&
(millis() - this->last_update_) > this->trigger_time_) {
future_state = this->desired_state_;
next_state = this->desired_state_;
}
bool delayed_sensor_not_ready = false;
bool instant_sensor_not_ready = false;
bool delayed_sensor_faulted = false;
bool instant_sensor_faulted = false;
#ifdef USE_BINARY_SENSOR
// Test all of the sensors in the list regardless of the alarm panel state
for (auto sensor_info : this->sensor_map_) {
// Test all of the sensors regardless of the alarm panel state
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for chime zones
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_CHIME)) {
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
// Look for the transition from closed to open
if ((!this->sensor_data_[sensor_info.second.store_index].last_chime_state) && (sensor_info.first->state)) {
if ((!this->sensor_data_[info.store_index].last_chime_state) && (sensor->state)) {
// Must be disarmed to chime
if (this->current_state_ == ACP_STATE_DISARMED) {
this->chime_callback_.call();
}
}
// Record the sensor state change
this->sensor_data_[sensor_info.second.store_index].last_chime_state = sensor_info.first->state;
this->sensor_data_[info.store_index].last_chime_state = sensor->state;
}
// Check for triggered sensors
if (sensor_info.first->state) { // Sensor triggered?
// Check for faulted sensors
if (sensor->state) {
// Skip if auto bypassed
if (std::count(this->bypassed_sensor_indicies_.begin(), this->bypassed_sensor_indicies_.end(),
sensor_info.second.store_index) == 1) {
info.store_index) == 1) {
continue;
}
// Skip if bypass armed home
if (this->current_state_ == ACP_STATE_ARMED_HOME &&
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
if ((this->current_state_ == ACP_STATE_ARMED_HOME) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME)) {
continue;
}
// Skip if bypass armed night
if (this->current_state_ == ACP_STATE_ARMED_NIGHT &&
(sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
if ((this->current_state_ == ACP_STATE_ARMED_NIGHT) && (info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT)) {
continue;
}
switch (sensor_info.second.type) {
case ALARM_SENSOR_TYPE_INSTANT:
instant_sensor_not_ready = true;
break;
switch (info.type) {
case ALARM_SENSOR_TYPE_INSTANT_ALWAYS:
instant_sensor_not_ready = true;
future_state = ACP_STATE_TRIGGERED;
next_state = ACP_STATE_TRIGGERED;
[[fallthrough]];
case ALARM_SENSOR_TYPE_INSTANT:
instant_sensor_faulted = true;
break;
case ALARM_SENSOR_TYPE_DELAYED_FOLLOWER:
// Look to see if we are in the pending state
if (this->current_state_ == ACP_STATE_PENDING) {
delayed_sensor_not_ready = true;
delayed_sensor_faulted = true;
} else {
instant_sensor_not_ready = true;
instant_sensor_faulted = true;
}
break;
case ALARM_SENSOR_TYPE_DELAYED:
default:
delayed_sensor_not_ready = true;
delayed_sensor_faulted = true;
}
}
}
// Update all sensors not ready flag
this->sensors_ready_ = ((!instant_sensor_not_ready) && (!delayed_sensor_not_ready));
// Update all sensors ready flag
bool sensors_ready = !(instant_sensor_faulted || delayed_sensor_faulted);
// Call the ready state change callback if there was a change
if (this->sensors_ready_ != this->sensors_ready_last_) {
if (this->sensors_ready_ != sensors_ready) {
this->sensors_ready_ = sensors_ready;
this->ready_callback_.call();
this->sensors_ready_last_ = this->sensors_ready_;
}
#endif
if (this->is_state_armed(future_state) && (!this->sensors_ready_)) {
if (this->is_state_armed(next_state) && (!this->sensors_ready_)) {
// Instant sensors
if (instant_sensor_not_ready) {
if (instant_sensor_faulted) {
this->publish_state(ACP_STATE_TRIGGERED);
} else if (delayed_sensor_not_ready) {
} else if (delayed_sensor_faulted) {
// Delayed sensors
if ((this->pending_time_ > 0) && (this->current_state_ != ACP_STATE_TRIGGERED)) {
this->publish_state(ACP_STATE_PENDING);
@@ -206,8 +195,8 @@ void TemplateAlarmControlPanel::loop() {
this->publish_state(ACP_STATE_TRIGGERED);
}
}
} else if (future_state != this->current_state_) {
this->publish_state(future_state);
} else if (next_state != this->current_state_) {
this->publish_state(next_state);
}
}
@@ -234,8 +223,6 @@ uint32_t TemplateAlarmControlPanel::get_supported_features() const {
return features;
}
bool TemplateAlarmControlPanel::get_requires_code() const { return !this->codes_.empty(); }
void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_panel::AlarmControlPanelState state,
uint32_t delay) {
if (this->current_state_ != ACP_STATE_DISARMED) {
@@ -257,11 +244,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) {
// Check for sensors left on and set to bypass automatically and remove them from monitoring
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for faulted bypass_auto sensors and remove them from monitoring
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(info.store_index);
}
}
#endif

View File

@@ -56,7 +56,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
void setup() override;
void loop() override;
uint32_t get_supported_features() const override;
bool get_requires_code() const override;
bool get_requires_code() const override { return !this->codes_.empty(); }
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
bool get_all_sensors_ready() { return this->sensors_ready_; };
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
@@ -66,7 +66,8 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
/** Add a binary_sensor to the alarm_panel.
*
* @param sensor The BinarySensor instance.
* @param ignore_when_home if this should be ignored when armed_home mode
* @param flags The OR of BinarySensorFlags for the sensor.
* @param type The sensor type which determines its triggering behaviour.
*/
void add_sensor(binary_sensor::BinarySensor *sensor, uint16_t flags = 0,
AlarmSensorType type = ALARM_SENSOR_TYPE_DELAYED);
@@ -121,7 +122,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR
// This maps a binary sensor to its type and attribute bits
// This maps a binary sensor to its alarm specific info
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
// a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_;
@@ -147,7 +148,6 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
bool supports_arm_home_ = false;
bool supports_arm_night_ = false;
bool sensors_ready_ = false;
bool sensors_ready_last_ = false;
uint8_t next_store_index_ = 0;
// check if the code is valid
bool is_code_valid_(optional<std::string> code);

View File

@@ -10,9 +10,6 @@ from .. import template_ns
TemplateBinarySensor = template_ns.class_(
"TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component
)
StatelessTemplateBinarySensor = template_ns.class_(
"StatelessTemplateBinarySensor", binary_sensor.BinarySensor, cg.Component
)
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(TemplateBinarySensor)
@@ -29,33 +26,28 @@ CONFIG_SCHEMA = (
async def to_code(config):
# Check if we have a lambda first - determines which class to instantiate
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
if lamb := config.get(CONF_LAMBDA):
# Use new_lambda_pvariable to create either TemplateBinarySensor or StatelessTemplateBinarySensor
template_ = await cg.process_lambda(
lamb, [], return_type=cg.optional.template(bool)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateBinarySensor
)
# Manually register as binary sensor since we didn't use new_binary_sensor
await binary_sensor.register_binary_sensor(var, config)
await cg.register_component(var, config)
elif condition := config.get(CONF_CONDITION):
# For conditions, create stateful version and set template
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
cg.add(var.set_template(template_))
if condition := config.get(CONF_CONDITION):
condition = await automation.build_condition(
condition, cg.TemplateArguments(), []
)
# Generate a stateless lambda that calls condition.check()
# capture="" is safe because condition is a global variable in generated C++ code
# and doesn't need to be captured. This allows implicit conversion to function pointer.
template_ = LambdaExpression(
f"return {condition.check()};", [], return_type=cg.optional.template(bool)
f"return {condition.check()};",
[],
return_type=cg.optional.template(bool),
capture="",
)
cg.add(var.set_template(template_))
else:
# No lambda or condition - just create the base template sensor
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
@automation.register_action(

View File

@@ -6,13 +6,18 @@ namespace template_ {
static const char *const TAG = "template.binary_sensor";
// Template instantiations
template<typename F> void TemplateBinarySensorBase<F>::dump_config() {
LOG_BINARY_SENSOR("", "Template Binary Sensor", this);
}
void TemplateBinarySensor::setup() { this->loop(); }
template class TemplateBinarySensorBase<std::function<optional<bool>()>>;
template class TemplateBinarySensorBase<optional<bool> (*)()>;
void TemplateBinarySensor::loop() {
if (!this->f_.has_value())
return;
auto s = (*this->f_)();
if (s.has_value()) {
this->publish_state(*s);
}
}
void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); }
} // namespace template_
} // namespace esphome

View File

@@ -6,41 +6,18 @@
namespace esphome {
namespace template_ {
template<typename F> class TemplateBinarySensorBase : public Component, public binary_sensor::BinarySensor {
class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor {
public:
void setup() override { this->loop(); }
void loop() override {
if (this->f_ == nullptr)
return;
auto s = this->f_();
if (s.has_value()) {
this->publish_state(*s);
}
}
void set_template(optional<bool> (*f)()) { this->f_ = f; }
void setup() override;
void loop() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::HARDWARE; }
protected:
F f_;
};
class TemplateBinarySensor : public TemplateBinarySensorBase<std::function<optional<bool>()>> {
public:
TemplateBinarySensor() { this->f_ = nullptr; }
void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; }
};
/** Optimized template binary sensor for stateless lambdas (no capture).
*
* Uses function pointer instead of std::function to reduce memory overhead.
* Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
*/
class StatelessTemplateBinarySensor : public TemplateBinarySensorBase<optional<bool> (*)()> {
public:
explicit StatelessTemplateBinarySensor(optional<bool> (*f)()) { this->f_ = f; }
optional<optional<bool> (*)()> f_;
};
} // namespace template_

View File

@@ -23,9 +23,6 @@ from esphome.const import (
from .. import template_ns
TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component)
StatelessTemplateCover = template_ns.class_(
"StatelessTemplateCover", cover.Cover, cg.Component
)
TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode")
RESTORE_MODES = {
@@ -66,22 +63,13 @@ CONFIG_SCHEMA = (
async def to_code(config):
var = await cover.new_cover(config)
await cg.register_component(var, config)
if CONF_LAMBDA in config:
# Use new_lambda_pvariable to create either TemplateCover or StatelessTemplateCover
template_ = await cg.process_lambda(
config[CONF_LAMBDA], [], return_type=cg.optional.template(float)
)
var = automation.new_lambda_pvariable(
config[CONF_ID], template_, StatelessTemplateCover
)
# Manually register as cover since we didn't use new_cover
await cover.register_cover(var, config)
await cg.register_component(var, config)
else:
# No state lambda - just create the base template cover
var = await cover.new_cover(config)
await cg.register_component(var, config)
cg.add(var.set_state_lambda(template_))
if CONF_OPEN_ACTION in config:
await automation.build_automation(
var.get_open_trigger(), [], config[CONF_OPEN_ACTION]

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