diff --git a/.clang-tidy.hash b/.clang-tidy.hash index 1cb5f98c28..ab354259e3 100644 --- a/.clang-tidy.hash +++ b/.clang-tidy.hash @@ -1 +1 @@ -cf3d341206b4184ec8b7fe85141aef4fe4696aa720c3f8a06d4e57930574bdab +069fa9526c52f7c580a9ec17c7678d12f142221387e9b561c18f95394d4629a3 diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index e178610a97..6d7d4f8c12 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: venv # yamllint disable-line rule:line-length diff --git a/.github/scripts/auto-label-pr/constants.js b/.github/scripts/auto-label-pr/constants.js index d5e42fa1b9..bd60d8c766 100644 --- a/.github/scripts/auto-label-pr/constants.js +++ b/.github/scripts/auto-label-pr/constants.js @@ -3,6 +3,7 @@ module.exports = { BOT_COMMENT_MARKER: '', CODEOWNERS_MARKER: '', TOO_BIG_MARKER: '', + DEPRECATED_COMPONENT_MARKER: '', MANAGED_LABELS: [ 'new-component', @@ -27,6 +28,7 @@ module.exports = { 'breaking-change', 'developer-breaking-change', 'code-quality', + 'deprecated-component' ], DOCS_PR_PATTERNS: [ diff --git a/.github/scripts/auto-label-pr/detectors.js b/.github/scripts/auto-label-pr/detectors.js index 79025988dd..f502a85666 100644 --- a/.github/scripts/auto-label-pr/detectors.js +++ b/.github/scripts/auto-label-pr/detectors.js @@ -251,6 +251,76 @@ async function detectPRTemplateCheckboxes(context) { return labels; } +// Strategy: Deprecated component detection +async function detectDeprecatedComponents(github, context, changedFiles) { + const labels = new Set(); + const deprecatedInfo = []; + const { owner, repo } = context.repo; + + // Compile regex once for better performance + const componentFileRegex = /^esphome\/components\/([^\/]+)\//; + + // Get files that are modified or added in components directory + const componentFiles = changedFiles.filter(file => componentFileRegex.test(file)); + + if (componentFiles.length === 0) { + return { labels, deprecatedInfo }; + } + + // Extract unique component names using the same regex + const components = new Set(); + for (const file of componentFiles) { + const match = file.match(componentFileRegex); + if (match) { + components.add(match[1]); + } + } + + // Get PR head to fetch files from the PR branch + const prNumber = context.payload.pull_request.number; + + // Check each component's __init__.py for DEPRECATED_COMPONENT constant + for (const component of components) { + const initFile = `esphome/components/${component}/__init__.py`; + try { + // Fetch file content from PR head using GitHub API + const { data: fileData } = await github.rest.repos.getContent({ + owner, + repo, + path: initFile, + ref: `refs/pull/${prNumber}/head` + }); + + // Decode base64 content + const content = Buffer.from(fileData.content, 'base64').toString('utf8'); + + // Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message' + // Support single quotes, double quotes, and triple quotes (for multiline) + const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) || + content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/); + const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) || + content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/); + const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch; + + if (deprecatedMatch) { + labels.add('deprecated-component'); + deprecatedInfo.push({ + component: component, + message: deprecatedMatch[1].trim() + }); + console.log(`Found deprecated component: ${component}`); + } + } catch (error) { + // Only log if it's not a simple "file not found" error (404) + if (error.status !== 404) { + console.log(`Error reading ${initFile}:`, error.message); + } + } + } + + return { labels, deprecatedInfo }; +} + // Strategy: Requirements detection async function detectRequirements(allLabels, prFiles, context) { const labels = new Set(); @@ -298,5 +368,6 @@ module.exports = { detectCodeOwner, detectTests, detectPRTemplateCheckboxes, + detectDeprecatedComponents, detectRequirements }; diff --git a/.github/scripts/auto-label-pr/index.js b/.github/scripts/auto-label-pr/index.js index 95ecfc4e33..483d2cb626 100644 --- a/.github/scripts/auto-label-pr/index.js +++ b/.github/scripts/auto-label-pr/index.js @@ -11,6 +11,7 @@ const { detectCodeOwner, detectTests, detectPRTemplateCheckboxes, + detectDeprecatedComponents, detectRequirements } = require('./detectors'); const { handleReviews } = require('./reviews'); @@ -112,6 +113,7 @@ module.exports = async ({ github, context }) => { codeOwnerLabels, testLabels, checkboxLabels, + deprecatedResult ] = await Promise.all([ detectMergeBranch(context), detectComponentPlatforms(changedFiles, apiData), @@ -124,8 +126,13 @@ module.exports = async ({ github, context }) => { detectCodeOwner(github, context, changedFiles), detectTests(changedFiles), detectPRTemplateCheckboxes(context), + detectDeprecatedComponents(github, context, changedFiles) ]); + // Extract deprecated component info + const deprecatedLabels = deprecatedResult.labels; + const deprecatedInfo = deprecatedResult.deprecatedInfo; + // Combine all labels const allLabels = new Set([ ...branchLabels, @@ -139,6 +146,7 @@ module.exports = async ({ github, context }) => { ...codeOwnerLabels, ...testLabels, ...checkboxLabels, + ...deprecatedLabels ]); // Detect requirements based on all other labels @@ -169,7 +177,7 @@ module.exports = async ({ github, context }) => { console.log('Computed labels:', finalLabels.join(', ')); // Handle reviews - await handleReviews(github, context, finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD); + await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD); // Apply labels await applyLabels(github, context, finalLabels); diff --git a/.github/scripts/auto-label-pr/reviews.js b/.github/scripts/auto-label-pr/reviews.js index a84e9ae5aa..906e2c456a 100644 --- a/.github/scripts/auto-label-pr/reviews.js +++ b/.github/scripts/auto-label-pr/reviews.js @@ -2,12 +2,29 @@ const { BOT_COMMENT_MARKER, CODEOWNERS_MARKER, TOO_BIG_MARKER, + DEPRECATED_COMPONENT_MARKER } = require('./constants'); // Generate review messages -function generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) { +function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) { const messages = []; + // Deprecated component message + if (finalLabels.includes('deprecated-component') && deprecatedInfo && deprecatedInfo.length > 0) { + let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`; + message += `Hey there @${prAuthor},\n`; + message += `This PR modifies one or more deprecated components. Please be aware:\n\n`; + + for (const info of deprecatedInfo) { + message += `#### Component: \`${info.component}\`\n`; + message += `${info.message}\n\n`; + } + + message += `Consider migrating to the recommended alternative if applicable.`; + + messages.push(message); + } + // Too big message if (finalLabels.includes('too-big')) { const testAdditions = prFiles @@ -54,14 +71,14 @@ function generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalA } // Handle reviews -async function handleReviews(github, context, finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) { +async function handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) { const { owner, repo } = context.repo; const pr_number = context.issue.number; const prAuthor = context.payload.pull_request.user.login; - const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD); + const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD); const hasReviewableLabels = finalLabels.some(label => - ['too-big', 'needs-codeowners'].includes(label) + ['too-big', 'needs-codeowners', 'deprecated-component'].includes(label) ); const { data: reviews } = await github.rest.pulls.listReviews({ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f521b07bae..841c297bce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: venv # yamllint disable-line rule:line-length @@ -157,7 +157,7 @@ jobs: token: ${{ secrets.CODECOV_TOKEN }} - name: Save Python virtual environment cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -193,7 +193,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Restore components graph cache - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -223,7 +223,7 @@ jobs: echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT - name: Save components graph cache if: github.ref == 'refs/heads/dev' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: .temp/components_graph.json key: components-graph-${{ hashFiles('esphome/components/**/*.py') }} @@ -245,7 +245,7 @@ jobs: python-version: "3.13" - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: venv key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }} @@ -334,14 +334,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} @@ -413,14 +413,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -502,14 +502,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }} @@ -735,7 +735,7 @@ jobs: - name: Restore cached memory analysis id: cache-memory-analysis if: steps.check-script.outputs.skip != 'true' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -759,7 +759,7 @@ jobs: - name: Cache platformio if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} @@ -800,7 +800,7 @@ jobs: - name: Save memory analysis to cache if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success' - uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: memory-analysis-target.json key: ${{ steps.cache-key.outputs.cache-key }} @@ -847,7 +847,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} cache-key: ${{ needs.common.outputs.cache-key }} - name: Cache platformio - uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 + uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 with: path: ~/.platformio key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index be761cee3d..51ea4085e0 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 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@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v4.32.0 + uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2 with: category: "/language:${{matrix.language}}" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b068673ecf..991e053d5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.14.14 + rev: v0.15.0 hooks: # Run the linter. - id: ruff diff --git a/CODEOWNERS b/CODEOWNERS index 00a22fed7c..25e6dc1b29 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -104,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris esphome/components/ccs811/* @habbie esphome/components/cd74hc4067/* @asoehlke esphome/components/ch422g/* @clydebarrow @jesterret +esphome/components/ch423/* @dwmw2 esphome/components/chsc6x/* @kkosik20 esphome/components/climate/* @esphome/core esphome/components/climate_ir/* @glmnet @@ -133,6 +134,7 @@ esphome/components/dfplayer/* @glmnet esphome/components/dfrobot_sen0395/* @niklasweber esphome/components/dht/* @OttoWinter esphome/components/display_menu_base/* @numo68 +esphome/components/dlms_meter/* @SimonFischer04 esphome/components/dps310/* @kbx81 esphome/components/ds1307/* @badbadc0ffee esphome/components/ds2484/* @mrk-its @@ -530,7 +532,7 @@ esphome/components/uart/packet_transport/* @clydebarrow esphome/components/udp/* @clydebarrow esphome/components/ufire_ec/* @pvizeli esphome/components/ufire_ise/* @pvizeli -esphome/components/ultrasonic/* @OttoWinter +esphome/components/ultrasonic/* @ssieb @swoboda1337 esphome/components/update/* @jesserockz esphome/components/uponor_smatrix/* @kroimon esphome/components/usb_cdc_acm/* @kbx81 diff --git a/esphome/__main__.py b/esphome/__main__.py index 55297e8d9b..c86b5604e1 100644 --- a/esphome/__main__.py +++ b/esphome/__main__.py @@ -294,8 +294,13 @@ def has_api() -> bool: def has_ota() -> bool: - """Check if OTA is available.""" - return CONF_OTA in CORE.config + """Check if OTA upload is available (requires platform: esphome).""" + if CONF_OTA not in CORE.config: + return False + return any( + ota_item.get(CONF_PLATFORM) == CONF_ESPHOME + for ota_item in CORE.config[CONF_OTA] + ) def has_mqtt_ip_lookup() -> bool: diff --git a/esphome/analyze_memory/__init__.py b/esphome/analyze_memory/__init__.py index 63ef0e74ed..d8c941e76f 100644 --- a/esphome/analyze_memory/__init__.py +++ b/esphome/analyze_memory/__init__.py @@ -12,7 +12,6 @@ from .const import ( CORE_SUBCATEGORY_PATTERNS, DEMANGLED_PATTERNS, ESPHOME_COMPONENT_PATTERN, - SECTION_TO_ATTR, SYMBOL_PATTERNS, ) from .demangle import batch_demangle @@ -91,6 +90,17 @@ class ComponentMemory: bss_size: int = 0 # Uninitialized data (ram only) symbol_count: int = 0 + def add_section_size(self, section_name: str, size: int) -> None: + """Add size to the appropriate attribute for a section.""" + if section_name == ".text": + self.text_size += size + elif section_name == ".rodata": + self.rodata_size += size + elif section_name == ".data": + self.data_size += size + elif section_name == ".bss": + self.bss_size += size + @property def flash_total(self) -> int: """Total flash usage (text + rodata + data).""" @@ -167,12 +177,15 @@ class MemoryAnalyzer: self._elf_symbol_names: set[str] = set() # SDK symbols not in ELF (static/local symbols from closed-source libs) self._sdk_symbols: list[SDKSymbol] = [] + # CSWTCH symbols: list of (name, size, source_file, component) + self._cswtch_symbols: list[tuple[str, int, str, str]] = [] def analyze(self) -> dict[str, ComponentMemory]: """Analyze the ELF file and return component memory usage.""" self._parse_sections() self._parse_symbols() self._categorize_symbols() + self._analyze_cswtch_symbols() self._analyze_sdk_libraries() return dict(self.components) @@ -255,8 +268,7 @@ class MemoryAnalyzer: comp_mem.symbol_count += 1 # Update the appropriate size attribute based on section - if attr_name := SECTION_TO_ATTR.get(section_name): - setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size) + comp_mem.add_section_size(section_name, size) # Track uncategorized symbols if component == "other" and size > 0: @@ -372,6 +384,205 @@ class MemoryAnalyzer: return "Other Core" + def _find_object_files_dir(self) -> Path | None: + """Find the directory containing object files for this build. + + Returns: + Path to the directory containing .o files, or None if not found. + """ + # The ELF is typically at .pioenvs//firmware.elf + # Object files are in .pioenvs//src/ and .pioenvs//lib*/ + pioenvs_dir = self.elf_path.parent + if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")): + return pioenvs_dir + return None + + def _scan_cswtch_in_objects( + self, obj_dir: Path + ) -> dict[str, list[tuple[str, int]]]: + """Scan object files for CSWTCH symbols using a single nm invocation. + + Uses ``nm --print-file-name -S`` on all ``.o`` files at once. + Output format: ``/path/to/file.o:address size type name`` + + Args: + obj_dir: Directory containing object files (.pioenvs//) + + Returns: + Dict mapping "CSWTCH$NNN:size" to list of (source_file, size) tuples. + """ + cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list) + + if not self.nm_path: + return cswtch_map + + # Find all .o files recursively, sorted for deterministic output + obj_files = sorted(obj_dir.rglob("*.o")) + if not obj_files: + return cswtch_map + + _LOGGER.debug("Scanning %d object files for CSWTCH symbols", len(obj_files)) + + # Single nm call with --print-file-name for all object files + result = run_tool( + [self.nm_path, "--print-file-name", "-S"] + [str(f) for f in obj_files], + timeout=30, + ) + if result is None or result.returncode != 0: + return cswtch_map + + for line in result.stdout.splitlines(): + if "CSWTCH$" not in line: + continue + + # Split on last ":" that precedes a hex address. + # nm --print-file-name format: filepath:hex_addr hex_size type name + # We split from the right: find the last colon followed by hex digits. + parts_after_colon = line.rsplit(":", 1) + if len(parts_after_colon) != 2: + continue + + file_path = parts_after_colon[0] + fields = parts_after_colon[1].split() + # fields: [address, size, type, name] + if len(fields) < 4: + continue + + sym_name = fields[3] + if not sym_name.startswith("CSWTCH$"): + continue + + try: + size = int(fields[1], 16) + except ValueError: + continue + + # Get relative path from obj_dir for readability + try: + rel_path = str(Path(file_path).relative_to(obj_dir)) + except ValueError: + rel_path = file_path + + key = f"{sym_name}:{size}" + cswtch_map[key].append((rel_path, size)) + + return cswtch_map + + def _source_file_to_component(self, source_file: str) -> str: + """Map a source object file path to its component name. + + Args: + source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o' + + Returns: + Component name like '[esphome]wifi' or the source file if unknown. + """ + parts = Path(source_file).parts + + # ESPHome component: src/esphome/components//... + if "components" in parts: + idx = parts.index("components") + if idx + 1 < len(parts): + component_name = parts[idx + 1] + if component_name in get_esphome_components(): + return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}" + if component_name in self.external_components: + return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}" + + # ESPHome core: src/esphome/core/... or src/esphome/... + if "core" in parts and "esphome" in parts: + return _COMPONENT_CORE + if "esphome" in parts and "components" not in parts: + return _COMPONENT_CORE + + # Framework/library files - return the first path component + # e.g., lib65b/ESPAsyncTCP/... -> lib65b + # FrameworkArduino/... -> FrameworkArduino + return parts[0] if parts else source_file + + def _analyze_cswtch_symbols(self) -> None: + """Analyze CSWTCH (GCC switch table) symbols by tracing to source objects. + + CSWTCH symbols are compiler-generated lookup tables for switch statements. + They are local symbols, so the same name can appear in different object files. + This method scans .o files to attribute them to their source components. + """ + obj_dir = self._find_object_files_dir() + if obj_dir is None: + _LOGGER.debug("No object files directory found, skipping CSWTCH analysis") + return + + # Scan object files for CSWTCH symbols + cswtch_map = self._scan_cswtch_in_objects(obj_dir) + if not cswtch_map: + _LOGGER.debug("No CSWTCH symbols found in object files") + return + + # Collect CSWTCH symbols from the ELF (already parsed in sections) + # Include section_name for re-attribution of component totals + elf_cswtch = [ + (symbol_name, size, section_name) + for section_name, section in self.sections.items() + for symbol_name, size, _ in section.symbols + if symbol_name.startswith("CSWTCH$") + ] + + _LOGGER.debug( + "Found %d CSWTCH symbols in ELF, %d unique in object files", + len(elf_cswtch), + len(cswtch_map), + ) + + # Match ELF CSWTCH symbols to source files and re-attribute component totals. + # _categorize_symbols() already ran and put these into "other" since CSWTCH$ + # names don't match any component pattern. We move the bytes to the correct + # component based on the object file mapping. + other_mem = self.components.get("other") + + for sym_name, size, section_name in elf_cswtch: + key = f"{sym_name}:{size}" + sources = cswtch_map.get(key, []) + + if len(sources) == 1: + source_file = sources[0][0] + component = self._source_file_to_component(source_file) + elif len(sources) > 1: + # Ambiguous - multiple object files have same CSWTCH name+size + source_file = "ambiguous" + component = "ambiguous" + _LOGGER.debug( + "Ambiguous CSWTCH %s (%d B) found in %d files: %s", + sym_name, + size, + len(sources), + ", ".join(src for src, _ in sources), + ) + else: + source_file = "unknown" + component = "unknown" + + self._cswtch_symbols.append((sym_name, size, source_file, component)) + + # Re-attribute from "other" to the correct component + if ( + component not in ("other", "unknown", "ambiguous") + and other_mem is not None + ): + other_mem.add_section_size(section_name, -size) + if component not in self.components: + self.components[component] = ComponentMemory(component) + self.components[component].add_section_size(section_name, size) + + # Sort by size descending + self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True) + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + _LOGGER.debug( + "CSWTCH analysis: %d symbols, %d bytes total", + len(self._cswtch_symbols), + total_size, + ) + def get_unattributed_ram(self) -> tuple[int, int, int]: """Get unattributed RAM sizes (SDK/framework overhead). diff --git a/esphome/analyze_memory/cli.py b/esphome/analyze_memory/cli.py index a77e17afce..bb0eb7723e 100644 --- a/esphome/analyze_memory/cli.py +++ b/esphome/analyze_memory/cli.py @@ -4,6 +4,8 @@ from __future__ import annotations from collections import defaultdict from collections.abc import Callable +import heapq +from operator import itemgetter import sys from typing import TYPE_CHECKING @@ -29,6 +31,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): ) # Lower threshold for RAM symbols (RAM is more constrained) RAM_SYMBOL_SIZE_THRESHOLD: int = 24 + # Number of top symbols to show in the largest symbols report + TOP_SYMBOLS_LIMIT: int = 30 + # Width for symbol name display in top symbols report + COL_TOP_SYMBOL_NAME: int = 55 # Column width constants COL_COMPONENT: int = 29 @@ -147,6 +153,83 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss] return f"{demangled} ({size:,} B){section_label}" + def _add_top_symbols(self, lines: list[str]) -> None: + """Add a section showing the top largest symbols in the binary.""" + # Collect all symbols from all components: (symbol, demangled, size, section, component) + all_symbols = [ + (symbol, demangled, size, section, component) + for component, symbols in self._component_symbols.items() + for symbol, demangled, size, section in symbols + ] + + # Get top N symbols by size using heapq for efficiency + top_symbols = heapq.nlargest( + self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2) + ) + + lines.append("") + lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:") + # Calculate truncation limit from column width (leaving room for "...") + truncate_limit = self.COL_TOP_SYMBOL_NAME - 3 + for i, (_, demangled, size, section, component) in enumerate(top_symbols): + # Format section label + section_label = f"[{section[1:]}]" if section else "" + # Truncate demangled name if too long + demangled_display = ( + f"{demangled[:truncate_limit]}..." + if len(demangled) > self.COL_TOP_SYMBOL_NAME + else demangled + ) + lines.append( + f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}" + ) + + def _add_cswtch_analysis(self, lines: list[str]) -> None: + """Add CSWTCH (GCC switch table lookup) analysis section.""" + self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)") + + total_size = sum(size for _, size, _, _ in self._cswtch_symbols) + lines.append( + f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B" + ) + lines.append("") + + # Group by component + by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list) + for sym_name, size, source_file, component in self._cswtch_symbols: + by_component[component].append((sym_name, size, source_file)) + + # Sort components by total size descending + sorted_components = sorted( + by_component.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ) + + for component, symbols in sorted_components: + comp_total = sum(s[1] for s in symbols) + lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):") + + # Group by source file within component + by_file: dict[str, list[tuple[str, int]]] = defaultdict(list) + for sym_name, size, source_file in symbols: + by_file[source_file].append((sym_name, size)) + + for source_file, file_symbols in sorted( + by_file.items(), + key=lambda x: sum(s[1] for s in x[1]), + reverse=True, + ): + file_total = sum(s[1] for s in file_symbols) + lines.append( + f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)" + ) + for sym_name, size in sorted( + file_symbols, key=lambda x: x[1], reverse=True + ): + lines.append(f" {size:>6,} B {sym_name}") + lines.append("") + def generate_report(self, detailed: bool = False) -> str: """Generate a formatted memory report.""" components = sorted( @@ -248,6 +331,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): "RAM", ) + # Top largest symbols in the binary + self._add_top_symbols(lines) + # Add ESPHome core detailed analysis if there are core symbols if self._esphome_core_symbols: self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis") @@ -431,6 +517,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer): lines.append(f" ... and {len(large_ram_syms) - 10} more") lines.append("") + # CSWTCH (GCC switch table) analysis + if self._cswtch_symbols: + self._add_cswtch_analysis(lines) + lines.append( "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included." ) diff --git a/esphome/analyze_memory/const.py b/esphome/analyze_memory/const.py index 83547b1eb5..66866615a6 100644 --- a/esphome/analyze_memory/const.py +++ b/esphome/analyze_memory/const.py @@ -66,15 +66,6 @@ SECTION_MAPPING = { ), } -# Section to ComponentMemory attribute mapping -# Maps section names to the attribute name in ComponentMemory dataclass -SECTION_TO_ATTR = { - ".text": "text_size", - ".rodata": "rodata_size", - ".data": "data_size", - ".bss": "bss_size", -} - # Component identification rules # Symbol patterns: patterns found in raw symbol names SYMBOL_PATTERNS = { @@ -513,7 +504,9 @@ SYMBOL_PATTERNS = { "__FUNCTION__$", "DAYS_IN_MONTH", "_DAYS_BEFORE_MONTH", - "CSWTCH$", + # Note: CSWTCH$ symbols are GCC switch table lookup tables. + # They are attributed to their source object files via _analyze_cswtch_symbols() + # rather than being lumped into libc. "dst$", "sulp", "_strtol_l", # String to long with locale diff --git a/esphome/components/absolute_humidity/absolute_humidity.cpp b/esphome/components/absolute_humidity/absolute_humidity.cpp index b13fcd519a..9c66531d05 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.cpp +++ b/esphome/components/absolute_humidity/absolute_humidity.cpp @@ -45,8 +45,6 @@ void AbsoluteHumidityComponent::dump_config() { this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str()); } -float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } - void AbsoluteHumidityComponent::loop() { if (!this->next_update_) { return; diff --git a/esphome/components/absolute_humidity/absolute_humidity.h b/esphome/components/absolute_humidity/absolute_humidity.h index 9f3b9eab8b..71feee2c42 100644 --- a/esphome/components/absolute_humidity/absolute_humidity.h +++ b/esphome/components/absolute_humidity/absolute_humidity.h @@ -24,7 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/adc/adc_sensor.h b/esphome/components/adc/adc_sensor.h index 526dd57fd5..91cf4eaafc 100644 --- a/esphome/components/adc/adc_sensor.h +++ b/esphome/components/adc/adc_sensor.h @@ -68,11 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage /// This method is called during the ESPHome setup process to log the configuration. void dump_config() override; - /// Return the setup priority for this component. - /// Components with higher priority are initialized earlier during setup. - /// @return A float representing the setup priority. - float get_setup_priority() const override; - #ifdef USE_ZEPHYR /// Set the ADC channel to be used by the ADC sensor. /// @param channel Pointer to an adc_dt_spec structure representing the ADC channel. diff --git a/esphome/components/adc/adc_sensor_common.cpp b/esphome/components/adc/adc_sensor_common.cpp index 748c8634b7..c779fd5893 100644 --- a/esphome/components/adc/adc_sensor_common.cpp +++ b/esphome/components/adc/adc_sensor_common.cpp @@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) { void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; } -float ADCSensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace adc } // namespace esphome diff --git a/esphome/components/adc/adc_sensor_esp32.cpp b/esphome/components/adc/adc_sensor_esp32.cpp index ea1263db5f..1d3138623e 100644 --- a/esphome/components/adc/adc_sensor_esp32.cpp +++ b/esphome/components/adc/adc_sensor_esp32.cpp @@ -42,11 +42,11 @@ void ADCSensor::setup() { adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize init_config.unit_id = this->adc_unit_; init_config.ulp_mode = ADC_ULP_MODE_DISABLE; -#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ - USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \ + USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT; -#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || - // USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 +#endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || + // USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]); if (err != ESP_OK) { ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err); @@ -76,7 +76,7 @@ void ADCSensor::setup() { #if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \ USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3 - // RISC-V variants and S3 use curve fitting calibration + // RISC-V variants (except C2) and S3 use curve fitting calibration adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) cali_config.chan = this->channel_; @@ -94,14 +94,14 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#else // Other ESP32 variants use line fitting calibration +#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration adc_cali_line_fitting_config_t cali_config = { .unit_id = this->adc_unit_, .atten = this->attenuation_, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, // Default reference voltage in mV -#endif // !defined(USE_ESP32_VARIANT_ESP32S2) +#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) }; err = adc_cali_create_scheme_line_fitting(&cali_config, &handle); if (err == ESP_OK) { @@ -112,7 +112,7 @@ void ADCSensor::setup() { ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err); this->setup_flags_.calibration_complete = false; } -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 } this->setup_flags_.init_complete = true; @@ -189,7 +189,7 @@ float ADCSensor::sample_fixed_attenuation_() { adc_cali_delete_scheme_curve_fitting(this->calibration_handle_); #else // Other ESP32 variants use line fitting calibration adc_cali_delete_scheme_line_fitting(this->calibration_handle_); -#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 +#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3 this->calibration_handle_ = nullptr; } } @@ -247,7 +247,7 @@ float ADCSensor::sample_autorange_() { .unit_id = this->adc_unit_, .atten = atten, .bitwidth = ADC_BITWIDTH_DEFAULT, -#if !defined(USE_ESP32_VARIANT_ESP32S2) +#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2) .default_vref = 1100, #endif }; diff --git a/esphome/components/adc/sensor.py b/esphome/components/adc/sensor.py index 64dd22b0c3..bab2762f00 100644 --- a/esphome/components/adc/sensor.py +++ b/esphome/components/adc/sensor.py @@ -2,7 +2,7 @@ import logging import esphome.codegen as cg from esphome.components import sensor, voltage_sampler -from esphome.components.esp32 import get_esp32_variant +from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC from esphome.components.zephyr import ( zephyr_add_overlay, @@ -118,6 +118,9 @@ async def to_code(config): cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE])) if CORE.is_esp32: + # Re-enable ESP-IDF's ADC driver (excluded by default to save compile time) + include_builtin_idf_component("esp_adc") + if attenuation := config.get(CONF_ATTENUATION): if attenuation == "auto": cg.add(var.set_autorange(cg.global_ns.true)) diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp index 03ce31d3cb..800b2d5261 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.cpp @@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor"; ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} -float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } - void ADC128S102Sensor::dump_config() { LOG_SENSOR("", "ADC128S102 Sensor", this); ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_); diff --git a/esphome/components/adc128s102/sensor/adc128s102_sensor.h b/esphome/components/adc128s102/sensor/adc128s102_sensor.h index 234500c2f4..5e6fc74e9c 100644 --- a/esphome/components/adc128s102/sensor/adc128s102_sensor.h +++ b/esphome/components/adc128s102/sensor/adc128s102_sensor.h @@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/aht10/aht10.cpp b/esphome/components/aht10/aht10.cpp index 03d9d9cd9e..1b1f8335cc 100644 --- a/esphome/components/aht10/aht10.cpp +++ b/esphome/components/aht10/aht10.cpp @@ -150,8 +150,6 @@ void AHT10Component::update() { this->restart_read_(); } -float AHT10Component::get_setup_priority() const { return setup_priority::DATA; } - void AHT10Component::dump_config() { ESP_LOGCONFIG(TAG, "AHT10:"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/aht10/aht10.h b/esphome/components/aht10/aht10.h index a3320c77e0..ce9cd963ad 100644 --- a/esphome/components/aht10/aht10.h +++ b/esphome/components/aht10/aht10.h @@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void set_variant(AHT10Variant variant) { this->variant_ = variant; } void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp index 862c620497..b8d246c861 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel_state.cpp @@ -1,32 +1,15 @@ #include "alarm_control_panel_state.h" +#include "esphome/core/progmem.h" namespace esphome::alarm_control_panel { +// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9) +PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT", + "ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN"); + const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { - switch (state) { - case ACP_STATE_DISARMED: - return LOG_STR("DISARMED"); - case ACP_STATE_ARMED_HOME: - return LOG_STR("ARMED_HOME"); - case ACP_STATE_ARMED_AWAY: - return LOG_STR("ARMED_AWAY"); - case ACP_STATE_ARMED_NIGHT: - return LOG_STR("ARMED_NIGHT"); - case ACP_STATE_ARMED_VACATION: - return LOG_STR("ARMED_VACATION"); - case ACP_STATE_ARMED_CUSTOM_BYPASS: - return LOG_STR("ARMED_CUSTOM_BYPASS"); - case ACP_STATE_PENDING: - return LOG_STR("PENDING"); - case ACP_STATE_ARMING: - return LOG_STR("ARMING"); - case ACP_STATE_DISARMING: - return LOG_STR("DISARMING"); - case ACP_STATE_TRIGGERED: - return LOG_STR("TRIGGERED"); - default: - return LOG_STR("UNKNOWN"); - } + return AlarmControlPanelStateStrings::get_log_str(static_cast(state), + AlarmControlPanelStateStrings::LAST_INDEX); } } // namespace esphome::alarm_control_panel diff --git a/esphome/components/am2315c/am2315c.cpp b/esphome/components/am2315c/am2315c.cpp index b20a8c6cbb..1390b74975 100644 --- a/esphome/components/am2315c/am2315c.cpp +++ b/esphome/components/am2315c/am2315c.cpp @@ -176,7 +176,5 @@ void AM2315C::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2315C::get_setup_priority() const { return setup_priority::DATA; } - } // namespace am2315c } // namespace esphome diff --git a/esphome/components/am2315c/am2315c.h b/esphome/components/am2315c/am2315c.h index c8d01beeaa..d7baf01cae 100644 --- a/esphome/components/am2315c/am2315c.h +++ b/esphome/components/am2315c/am2315c.h @@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; void setup() override; - float get_setup_priority() const override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } diff --git a/esphome/components/am2320/am2320.cpp b/esphome/components/am2320/am2320.cpp index 055be2aeee..7fef3bb3a6 100644 --- a/esphome/components/am2320/am2320.cpp +++ b/esphome/components/am2320/am2320.cpp @@ -51,7 +51,6 @@ void AM2320Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float AM2320Component::get_setup_priority() const { return setup_priority::DATA; } bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) { if (!this->write_bytes(a_register, data, 2)) { diff --git a/esphome/components/am2320/am2320.h b/esphome/components/am2320/am2320.h index da1e87cf65..708dbb632e 100644 --- a/esphome/components/am2320/am2320.h +++ b/esphome/components/am2320/am2320.h @@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/apds9960/apds9960.cpp b/esphome/components/apds9960/apds9960.cpp index 93038d3160..260de82d14 100644 --- a/esphome/components/apds9960/apds9960.cpp +++ b/esphome/components/apds9960/apds9960.cpp @@ -384,7 +384,6 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) { } } } -float APDS9960::get_setup_priority() const { return setup_priority::DATA; } bool APDS9960::is_proximity_enabled_() const { return #ifdef USE_SENSOR diff --git a/esphome/components/apds9960/apds9960.h b/esphome/components/apds9960/apds9960.h index 2a0fbb5c19..4574b70a42 100644 --- a/esphome/components/apds9960/apds9960.h +++ b/esphome/components/apds9960/apds9960.h @@ -32,7 +32,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 597da25883..d25934c60b 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -45,6 +45,7 @@ service APIConnection { rpc time_command (TimeCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {} rpc valve_command (ValveCommandRequest) returns (void) {} + rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 1626f395e6..2aa5956f24 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -300,7 +300,7 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { // Encodes a message to the buffer and returns the total number of bytes used, // including header and footer overhead. Returns 0 if the message doesn't fit. uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { #ifdef HAS_PROTO_MESSAGE_DUMP // If in log-only mode, just log and return if (conn->flags_.log_only_mode) { @@ -330,12 +330,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess // Get buffer size after allocation (which includes header padding) std::vector &shared_buf = conn->parent_->get_shared_buffer_ref(); - if (is_single || conn->flags_.batch_first_message) { - // Single message or first batch message - conn->prepare_first_message_buffer(shared_buf, header_padding, total_calculated_size); - if (conn->flags_.batch_first_message) { - conn->flags_.batch_first_message = false; - } + if (conn->flags_.batch_first_message) { + // First message - buffer already prepared by caller, just clear flag + conn->flags_.batch_first_message = false; } else { // Batch message second or later // Add padding for previous message footer + this message header @@ -365,24 +362,22 @@ bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary BinarySensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); BinarySensorStateResponse resp; resp.state = binary_sensor->state; resp.missing_state = !binary_sensor->has_state(); return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } -uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *binary_sensor = static_cast(entity); ListEntitiesBinarySensorResponse msg; msg.device_class = binary_sensor->get_device_class_ref(); msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -390,8 +385,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne bool APIConnection::send_cover_state(cover::Cover *cover) { return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast(entity); CoverStateResponse msg; auto traits = cover->get_traits(); @@ -399,10 +393,9 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * if (traits.get_supports_tilt()) msg.tilt = cover->tilt; msg.current_operation = static_cast(cover->current_operation); - return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *cover = static_cast(entity); ListEntitiesCoverResponse msg; auto traits = cover->get_traits(); @@ -411,8 +404,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c msg.supports_tilt = traits.get_supports_tilt(); msg.supports_stop = traits.get_supports_stop(); msg.device_class = cover->get_device_class_ref(); - return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::cover_command(const CoverCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover) @@ -430,8 +422,7 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { bool APIConnection::send_fan_state(fan::Fan *fan) { return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast(entity); FanStateResponse msg; auto traits = fan->get_traits(); @@ -445,10 +436,9 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co msg.direction = static_cast(fan->direction); if (traits.supports_preset_modes() && fan->has_preset_mode()) msg.preset_mode = fan->get_preset_mode(); - return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *fan = static_cast(entity); ListEntitiesFanResponse msg; auto traits = fan->get_traits(); @@ -457,7 +447,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con msg.supports_direction = traits.supports_direction(); msg.supported_speed_count = traits.supported_speed_count(); msg.supported_preset_modes = &traits.supported_preset_modes(); - return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::fan_command(const FanCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan) @@ -481,8 +471,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { bool APIConnection::send_light_state(light::LightState *light) { return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast(entity); LightStateResponse resp; auto values = light->remote_values; @@ -501,10 +490,9 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * if (light->supports_effects()) { resp.effect = light->get_effect_name(); } - return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *light = static_cast(entity); ListEntitiesLightResponse msg; auto traits = light->get_traits(); @@ -527,8 +515,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c } } msg.effects = &effects_list; - return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::light_command(const LightCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light) @@ -568,17 +555,15 @@ bool APIConnection::send_sensor_state(sensor::Sensor *sensor) { return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast(entity); SensorStateResponse resp; resp.state = sensor->state; resp.missing_state = !sensor->has_state(); - return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *sensor = static_cast(entity); ListEntitiesSensorResponse msg; msg.unit_of_measurement = sensor->get_unit_of_measurement_ref(); @@ -586,8 +571,7 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection * msg.force_update = sensor->get_force_update(); msg.device_class = sensor->get_device_class_ref(); msg.state_class = static_cast(sensor->get_state_class()); - return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -596,23 +580,19 @@ bool APIConnection::send_switch_state(switch_::Switch *a_switch) { return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast(entity); SwitchStateResponse resp; resp.state = a_switch->state; - return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_switch = static_cast(entity); ListEntitiesSwitchResponse msg; msg.assumed_state = a_switch->assumed_state(); msg.device_class = a_switch->get_device_class_ref(); - return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::switch_command(const SwitchCommandRequest &msg) { ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch) @@ -631,22 +611,19 @@ bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) TextSensorStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); TextSensorStateResponse resp; resp.state = StringRef(text_sensor->state); resp.missing_state = !text_sensor->has_state(); - return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text_sensor = static_cast(entity); ListEntitiesTextSensorResponse msg; msg.device_class = text_sensor->get_device_class_ref(); return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } #endif @@ -654,8 +631,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect bool APIConnection::send_climate_state(climate::Climate *climate) { return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast(entity); ClimateStateResponse resp; auto traits = climate->get_traits(); @@ -687,11 +663,9 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection resp.current_humidity = climate->current_humidity; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) resp.target_humidity = climate->target_humidity; - return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *climate = static_cast(entity); ListEntitiesClimateResponse msg; auto traits = climate->get_traits(); @@ -716,8 +690,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection msg.supported_presets = &traits.get_supported_presets(); msg.supported_custom_presets = &traits.get_supported_custom_presets(); msg.supported_swing_modes = &traits.get_supported_swing_modes(); - return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::climate_command(const ClimateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate) @@ -750,17 +723,15 @@ bool APIConnection::send_number_state(number::Number *number) { return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); NumberStateResponse resp; resp.state = number->state; resp.missing_state = !number->has_state(); - return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *number = static_cast(entity); ListEntitiesNumberResponse msg; msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref(); @@ -769,8 +740,7 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection * msg.min_value = number->traits.get_min_value(); msg.max_value = number->traits.get_max_value(); msg.step = number->traits.get_step(); - return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::number_command(const NumberCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(number::Number, number, number) @@ -783,22 +753,19 @@ void APIConnection::number_command(const NumberCommandRequest &msg) { bool APIConnection::send_date_state(datetime::DateEntity *date) { return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast(entity); DateStateResponse resp; resp.missing_state = !date->has_state(); resp.year = date->year; resp.month = date->month; resp.day = date->day; - return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *date = static_cast(entity); ListEntitiesDateResponse msg; - return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::date_command(const DateCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date) @@ -811,22 +778,19 @@ void APIConnection::date_command(const DateCommandRequest &msg) { bool APIConnection::send_time_state(datetime::TimeEntity *time) { return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast(entity); TimeStateResponse resp; resp.missing_state = !time->has_state(); resp.hour = time->hour; resp.minute = time->minute; resp.second = time->second; - return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *time = static_cast(entity); ListEntitiesTimeResponse msg; - return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::time_command(const TimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time) @@ -840,8 +804,7 @@ bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) { return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE, DateTimeStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast(entity); DateTimeStateResponse resp; resp.missing_state = !datetime->has_state(); @@ -849,15 +812,12 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio ESPTime state = datetime->state_as_esptime(); resp.epoch_seconds = state.timestamp; } - return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *datetime = static_cast(entity); ListEntitiesDateTimeResponse msg; - return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::datetime_command(const DateTimeCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime) @@ -871,25 +831,22 @@ bool APIConnection::send_text_state(text::Text *text) { return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast(entity); TextStateResponse resp; resp.state = StringRef(text->state); resp.missing_state = !text->has_state(); - return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *text = static_cast(entity); ListEntitiesTextResponse msg; msg.mode = static_cast(text->traits.get_mode()); msg.min_length = text->traits.get_min_length(); msg.max_length = text->traits.get_max_length(); msg.pattern = text->traits.get_pattern_ref(); - return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::text_command(const TextCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(text::Text, text, text) @@ -903,22 +860,19 @@ bool APIConnection::send_select_state(select::Select *select) { return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast(entity); SelectStateResponse resp; resp.state = select->current_option(); resp.missing_state = !select->has_state(); - return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *select = static_cast(entity); ListEntitiesSelectResponse msg; msg.options = &select->traits.get_options(); - return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::select_command(const SelectCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(select::Select, select, select) @@ -928,13 +882,11 @@ void APIConnection::select_command(const SelectCommandRequest &msg) { #endif #ifdef USE_BUTTON -uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *button = static_cast(entity); ListEntitiesButtonResponse msg; msg.device_class = button->get_device_class_ref(); - return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size); } void esphome::api::APIConnection::button_command(const ButtonCommandRequest &msg) { ENTITY_COMMAND_GET(button::Button, button, button) @@ -947,23 +899,20 @@ bool APIConnection::send_lock_state(lock::Lock *a_lock) { return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast(entity); LockStateResponse resp; resp.state = static_cast(a_lock->state); - return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *a_lock = static_cast(entity); ListEntitiesLockResponse msg; msg.assumed_state = a_lock->traits.get_assumed_state(); msg.supports_open = a_lock->traits.get_supports_open(); msg.requires_code = a_lock->traits.get_requires_code(); - return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::lock_command(const LockCommandRequest &msg) { ENTITY_COMMAND_GET(lock::Lock, a_lock, lock) @@ -986,16 +935,14 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { bool APIConnection::send_valve_state(valve::Valve *valve) { return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast(entity); ValveStateResponse resp; resp.position = valve->position; resp.current_operation = static_cast(valve->current_operation); - return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *valve = static_cast(entity); ListEntitiesValveResponse msg; auto traits = valve->get_traits(); @@ -1003,8 +950,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c msg.assumed_state = traits.get_is_assumed_state(); msg.supports_position = traits.get_supports_position(); msg.supports_stop = traits.get_supports_stop(); - return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::valve_command(const ValveCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve) @@ -1021,8 +967,7 @@ bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_pla return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE, MediaPlayerStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast(entity); MediaPlayerStateResponse resp; media_player::MediaPlayerState report_state = media_player->state == media_player::MEDIA_PLAYER_STATE_ANNOUNCING @@ -1031,11 +976,9 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne resp.state = static_cast(report_state); resp.volume = media_player->volume; resp.muted = media_player->is_muted(); - return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *media_player = static_cast(entity); ListEntitiesMediaPlayerResponse msg; auto traits = media_player->get_traits(); @@ -1051,7 +994,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec media_format.sample_bytes = supported_format.sample_bytes; } return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player) @@ -1092,7 +1035,7 @@ void APIConnection::try_send_camera_image_() { msg.device_id = camera::Camera::instance()->get_device_id(); #endif - if (!this->send_message_(msg, CameraImageResponse::MESSAGE_TYPE)) { + if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) { return; // Send failed, try again later } this->image_reader_->consume_data(to_send); @@ -1115,12 +1058,10 @@ void APIConnection::set_camera_state(std::shared_ptr image) this->try_send_camera_image_(); } } -uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *camera = static_cast(entity); ListEntitiesCameraResponse msg; - return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::camera_image(const CameraImageRequest &msg) { if (camera::Camera::instance() == nullptr) @@ -1305,22 +1246,22 @@ bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmCon AlarmControlPanelStateResponse::ESTIMATED_SIZE); } uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast(entity); AlarmControlPanelStateResponse resp; resp.state = static_cast(a_alarm_control_panel->get_state()); return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, - remaining_size, is_single); + remaining_size); } uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { auto *a_alarm_control_panel = static_cast(entity); ListEntitiesAlarmControlPanelResponse msg; msg.supported_features = a_alarm_control_panel->get_supported_features(); msg.requires_code = a_alarm_control_panel->get_requires_code(); msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE, - conn, remaining_size, is_single); + conn, remaining_size); } void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel) @@ -1357,8 +1298,7 @@ bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_hea return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE, WaterHeaterStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); WaterHeaterStateResponse resp; resp.mode = static_cast(wh->get_mode()); @@ -1369,10 +1309,9 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne resp.state = wh->get_state(); resp.key = wh->get_object_id_hash(); - return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *wh = static_cast(entity); ListEntitiesWaterHeaterResponse msg; auto traits = wh->get_traits(); @@ -1381,11 +1320,10 @@ uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnec msg.target_temperature_step = traits.get_target_temperature_step(); msg.supported_modes = &traits.get_supported_modes(); msg.supported_features = traits.get_feature_flags(); - return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size); } -void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { +void APIConnection::water_heater_command(const WaterHeaterCommandRequest &msg) { ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater) if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE) call.set_mode(static_cast(msg.mode)); @@ -1411,20 +1349,18 @@ void APIConnection::send_event(event::Event *event) { event->get_last_event_type_index()); } uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single) { + uint32_t remaining_size) { EventResponse resp; resp.event_type = event_type; - return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *event = static_cast(entity); ListEntitiesEventResponse msg; msg.device_class = event->get_device_class_ref(); msg.event_types = &event->get_event_types(); - return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1447,13 +1383,11 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent #endif #ifdef USE_INFRARED -uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *infrared = static_cast(entity); ListEntitiesInfraredResponse msg; msg.capabilities = infrared->get_capability_flags(); - return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size); } #endif @@ -1461,8 +1395,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE); } -uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); UpdateStateResponse resp; resp.missing_state = !update->has_state(); @@ -1478,15 +1411,13 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection resp.release_summary = StringRef(update->update_info.summary); resp.release_url = StringRef(update->update_info.release_url); } - return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { auto *update = static_cast(entity); ListEntitiesUpdateResponse msg; msg.device_class = update->get_device_class_ref(); - return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size, - is_single); + return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size); } void APIConnection::update_command(const UpdateCommandRequest &msg) { ENTITY_COMMAND_GET(update::UpdateEntity, update, update) @@ -1512,7 +1443,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char SubscribeLogsResponse msg; msg.level = static_cast(level); msg.set_message(reinterpret_cast(line), message_len); - return this->send_message_(msg, SubscribeLogsResponse::MESSAGE_TYPE); + return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE); } void APIConnection::complete_authentication_() { @@ -1837,6 +1768,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { } return false; } +bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) { + ProtoSize size; + msg.calculate_size(size); + std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, size.get_size()); + msg.encode({&shared_buf}); + return this->send_buffer({&shared_buf}, message_type); +} bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE); @@ -1897,6 +1836,23 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me } } +bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, + uint8_t aux_data_index) { + if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { + auto &shared_buf = this->parent_->get_shared_buffer_ref(); + this->prepare_first_message_buffer(shared_buf, estimated_size); + DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; + if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && + this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) { +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_batch_item_(item); +#endif + return true; + } + } + return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); +} + bool APIConnection::schedule_batch_() { if (!this->flags_.batch_scheduled) { this->flags_.batch_scheduled = true; @@ -1925,10 +1881,21 @@ void APIConnection::process_batch_() { auto &shared_buf = this->parent_->get_shared_buffer_ref(); size_t num_items = this->deferred_batch_.size(); - // Fast path for single message - allocate exact size needed + // Cache these values to avoid repeated virtual calls + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + + // Pre-calculate exact buffer size needed based on message types + uint32_t total_estimated_size = num_items * (header_padding + footer_size); + for (size_t i = 0; i < num_items; i++) { + total_estimated_size += this->deferred_batch_[i].estimated_size; + } + + this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size); + + // Fast path for single message - buffer already allocated above if (num_items == 1) { const auto &item = this->deferred_batch_[0]; - // Let dispatch_message_ calculate size and encode if it fits uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits::max(), true); @@ -1951,30 +1918,8 @@ void APIConnection::process_batch_() { // Stack-allocated array for message info alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)]; MessageInfo *message_info = reinterpret_cast(message_info_storage); - size_t message_count = 0; - - // Cache these values to avoid repeated virtual calls - const uint8_t header_padding = this->helper_->frame_header_padding(); - const uint8_t footer_size = this->helper_->frame_footer_size(); - - // Initialize buffer and tracking variables - shared_buf.clear(); - - // Pre-calculate exact buffer size needed based on message types - uint32_t total_estimated_size = num_items * (header_padding + footer_size); - for (size_t i = 0; i < this->deferred_batch_.size(); i++) { - const auto &item = this->deferred_batch_[i]; - total_estimated_size += item.estimated_size; - } - - // Calculate total overhead for all messages - // Reserve based on estimated size (much more accurate than 24-byte worst-case) - shared_buf.reserve(total_estimated_size); - this->flags_.batch_first_message = true; - size_t items_processed = 0; uint16_t remaining_size = std::numeric_limits::max(); - // Track where each message's header padding begins in the buffer // For plaintext: this is where the 6-byte header padding starts // For noise: this is where the 7-byte header padding starts @@ -1986,7 +1931,7 @@ void APIConnection::process_batch_() { const auto &item = this->deferred_batch_[i]; // Try to encode message via dispatch // The dispatch function calculates overhead to determine if the message fits - uint16_t payload_size = this->dispatch_message_(item, remaining_size, false); + uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0); if (payload_size == 0) { // Message won't fit, stop processing @@ -2000,10 +1945,7 @@ void APIConnection::process_batch_() { // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements // Explicit destruction is not needed because MessageInfo is trivially destructible, // as ensured by the static_assert in its definition. - new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size); - - // Update tracking variables - items_processed++; + new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size); // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation if (items_processed == 1) { remaining_size = MAX_BATCH_PACKET_SIZE; @@ -2026,7 +1968,7 @@ void APIConnection::process_batch_() { // Send all collected messages APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf}, - std::span(message_info, message_count)); + std::span(message_info, items_processed)); if (err != APIError::OK && err != APIError::WOULD_BLOCK) { this->fatal_error_with_log_(LOG_STR("Batch write failed"), err); } @@ -2055,7 +1997,8 @@ void APIConnection::process_batch_() { // Dispatch message encoding based on message_type // Switch assigns function pointer, single call site for smaller code size uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, - bool is_single) { + bool batch_first) { + this->flags_.batch_first_message = batch_first; #ifdef USE_EVENT // Events need aux_data_index to look up event type from entity if (item.message_type == EventResponse::MESSAGE_TYPE) { @@ -2064,7 +2007,7 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, return 0; auto *event = static_cast(item.entity); return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)), - this, remaining_size, is_single); + this, remaining_size); } #endif @@ -2174,25 +2117,22 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, #undef CASE_STATE_INFO #undef CASE_INFO_ONLY - return func(item.entity, this, remaining_size, is_single); + return func(item.entity, this, remaining_size); } -uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { ListEntitiesDoneResponse resp; - return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { DisconnectRequest req; - return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size); } -uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single) { +uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) { PingRequest req; - return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single); + return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size); } #ifdef USE_API_HOMEASSISTANT_STATES diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 21bf4c4073..40e4fd61c1 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -170,7 +170,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_WATER_HEATER bool send_water_heater_state(water_heater::WaterHeater *water_heater); - void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; + void water_heater_command(const WaterHeaterCommandRequest &msg) override; #endif #ifdef USE_IR_RF @@ -255,17 +255,7 @@ class APIConnection final : public APIServerConnection { void on_fatal_error() override; void on_no_setup_connection() override; - ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { - // FIXME: ensure no recursive writes can happen - - // Get header padding size - used for both reserve and insert - uint8_t header_padding = this->helper_->frame_header_padding(); - // Get shared buffer from parent server - std::vector &shared_buf = this->parent_->get_shared_buffer_ref(); - this->prepare_first_message_buffer(shared_buf, header_padding, - reserve_size + header_padding + this->helper_->frame_footer_size()); - return {&shared_buf}; - } + bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override; void prepare_first_message_buffer(std::vector &shared_buf, size_t header_padding, size_t total_size) { shared_buf.clear(); @@ -277,6 +267,13 @@ class APIConnection final : public APIServerConnection { shared_buf.resize(header_padding); } + // Convenience overload - computes frame overhead internally + void prepare_first_message_buffer(std::vector &shared_buf, size_t payload_size) { + const uint8_t header_padding = this->helper_->frame_header_padding(); + const uint8_t footer_size = this->helper_->frame_footer_size(); + this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size); + } + bool try_to_clear_buffer(bool log_out_of_space); bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override; @@ -298,21 +295,21 @@ class APIConnection final : public APIServerConnection { // Non-template helper to encode any ProtoMessage static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); + uint32_t remaining_size); // Helper to fill entity state base and encode message static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { msg.key = entity->get_object_id_hash(); #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } // Helper to fill entity info base and encode message static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type, - APIConnection *conn, uint32_t remaining_size, bool is_single) { + APIConnection *conn, uint32_t remaining_size) { // Set common fields that are shared by all entity types msg.key = entity->get_object_id_hash(); @@ -339,7 +336,7 @@ class APIConnection final : public APIServerConnection { #ifdef USE_DEVICES msg.device_id = entity->get_device_id(); #endif - return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single); + return encode_message_to_buffer(msg, message_type, conn, remaining_size); } #ifdef USE_VOICE_ASSISTANT @@ -370,141 +367,108 @@ class APIConnection final : public APIServerConnection { } #ifdef USE_BINARY_SENSOR - static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_COVER - static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_FAN - static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LIGHT - static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SENSOR - static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SWITCH - static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT_SENSOR - static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CLIMATE - static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_NUMBER - static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATE - static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_TIME - static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_DATETIME_DATETIME - static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_TEXT - static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_SELECT - static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_BUTTON - static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_LOCK - static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); - static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_VALVE - static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_MEDIA_PLAYER - static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_ALARM_CONTROL_PANEL - static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_WATER_HEATER - static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_INFRARED - static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, - uint32_t remaining_size, bool is_single); - static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); + uint32_t remaining_size); + static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_UPDATE - static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); - static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); + static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif #ifdef USE_CAMERA - static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); #endif // Method for ListEntitiesDone batching - static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Method for DisconnectRequest batching - static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // Batch message method for ping requests - static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, - bool is_single); + static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size); // === Optimal member ordering for 32-bit systems === @@ -539,7 +503,7 @@ class APIConnection final : public APIServerConnection { #endif // Function pointer type for message encoding - using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); + using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size); // Generic batching mechanism for both state updates and entity info struct DeferredBatch { @@ -652,7 +616,7 @@ class APIConnection final : public APIServerConnection { // Dispatch message encoding based on message_type - replaces function pointer storage // Switch assigns pointer, single call site for smaller code size - uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single); + uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first); #ifdef HAS_PROTO_MESSAGE_DUMP void log_batch_item_(const DeferredBatch::BatchItem &item) { @@ -684,19 +648,7 @@ class APIConnection final : public APIServerConnection { // Tries immediate send if should_send_immediately_() returns true and buffer has space // Falls back to batching if immediate send fails or isn't applicable bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, - uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) { - if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) { - DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index}; - if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) && - this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) { -#ifdef HAS_PROTO_MESSAGE_DUMP - this->log_batch_item_(item); -#endif - return true; - } - } - return this->schedule_message_(entity, message_type, estimated_size, aux_data_index); - } + uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED); // Helper function to schedule a deferred message with known message type bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size, diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 29121f05e0..e9db36ae21 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -23,15 +23,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -49,31 +42,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -112,7 +97,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\n"); } template<> const char *proto_enum_to_string(enums::EntityCategory value) { diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index 4b7148e6c0..af0a2d0ca2 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -746,6 +746,11 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest & #ifdef USE_VALVE void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); } #endif +#ifdef USE_WATER_HEATER +void APIServerConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) { + this->water_heater_command(msg); +} +#endif #ifdef USE_BLUETOOTH_PROXY void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( const SubscribeBluetoothLEAdvertisementsRequest &msg) { diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index 200991c282..e2bc1609ed 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -23,7 +23,7 @@ class APIServerConnectionBase : public ProtoService { DumpBuffer dump_buf; this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf)); #endif - return this->send_message_(msg, message_type); + return this->send_message_impl(msg, message_type); } virtual void on_hello_request(const HelloRequest &value){}; @@ -303,6 +303,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VALVE virtual void valve_command(const ValveCommandRequest &msg) = 0; #endif +#ifdef USE_WATER_HEATER + virtual void water_heater_command(const WaterHeaterCommandRequest &msg) = 0; +#endif #ifdef USE_BLUETOOTH_PROXY virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; #endif @@ -432,6 +435,9 @@ class APIServerConnection : public APIServerConnectionBase { #ifdef USE_VALVE void on_valve_command_request(const ValveCommandRequest &msg) override; #endif +#ifdef USE_WATER_HEATER + void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; +#endif #ifdef USE_BLUETOOTH_PROXY void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; #endif diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index ed97c3b9a2..c56449455d 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -211,7 +211,7 @@ void APIServer::loop() { #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER // Fire trigger after client is removed so api.connected reflects the true state - this->client_disconnected_trigger_->trigger(client_name, client_peername); + this->client_disconnected_trigger_.trigger(client_name, client_peername); #endif // Don't increment client_index since we need to process the swapped element } diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 93421ef801..6ab3cdc576 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -227,12 +227,10 @@ class APIServer : public Component, #endif #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *get_client_connected_trigger() const { return this->client_connected_trigger_; } + Trigger *get_client_connected_trigger() { return &this->client_connected_trigger_; } #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *get_client_disconnected_trigger() const { - return this->client_disconnected_trigger_; - } + Trigger *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; } #endif protected: @@ -253,10 +251,10 @@ class APIServer : public Component, // Pointers and pointer-like types first (4 bytes each) std::unique_ptr socket_ = nullptr; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER - Trigger *client_connected_trigger_ = new Trigger(); + Trigger client_connected_trigger_; #endif #ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER - Trigger *client_disconnected_trigger_ = new Trigger(); + Trigger client_disconnected_trigger_; #endif // 4-byte aligned types diff --git a/esphome/components/api/homeassistant_service.h b/esphome/components/api/homeassistant_service.h index 9bffe18764..8ee23c75fe 100644 --- a/esphome/components/api/homeassistant_service.h +++ b/esphome/components/api/homeassistant_service.h @@ -136,12 +136,10 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response = true; } #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; - } + Trigger *get_success_trigger_with_response() { return &this->success_trigger_with_response_; } #endif - Trigger *get_success_trigger() const { return this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_success_trigger() { return &this->success_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES void play(const Ts &...x) override { @@ -187,14 +185,14 @@ template class HomeAssistantServiceCallAction : public Actionflags_.wants_response) { - this->success_trigger_with_response_->trigger(response.get_json(), args...); + this->success_trigger_with_response_.trigger(response.get_json(), args...); } else #endif { - this->success_trigger_->trigger(args...); + this->success_trigger_.trigger(args...); } } else { - this->error_trigger_->trigger(response.get_error_message(), args...); + this->error_trigger_.trigger(response.get_error_message(), args...); } }, captured_args); @@ -251,10 +249,10 @@ template class HomeAssistantServiceCallAction : public Action response_template_{""}; - Trigger *success_trigger_with_response_ = new Trigger(); + Trigger success_trigger_with_response_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - Trigger *success_trigger_ = new Trigger(); - Trigger *error_trigger_ = new Trigger(); + Trigger success_trigger_; + Trigger error_trigger_; #endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES struct Flags { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 2e0df297c3..92978f765f 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -402,6 +402,20 @@ class DumpBuffer { const char *c_str() const { return buf_; } size_t size() const { return pos_; } + /// Get writable buffer pointer for use with buf_append_printf + char *data() { return buf_; } + /// Get current position for use with buf_append_printf + size_t pos() const { return pos_; } + /// Update position after buf_append_printf call + void set_pos(size_t pos) { + if (pos >= CAPACITY) { + pos_ = CAPACITY - 1; + } else { + pos_ = pos; + } + buf_[pos_] = '\0'; + } + private: void append_impl_(const char *str, size_t len) { size_t space = CAPACITY - 1 - pos_; @@ -943,32 +957,16 @@ class ProtoService { virtual bool is_connection_setup() = 0; virtual void on_fatal_error() = 0; virtual void on_no_setup_connection() = 0; - /** - * Create a buffer with a reserved size. - * @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint - * to optimize memory usage and avoid reallocations during encoding. - * Implementations should aim to allocate at least this size. - * @return A ProtoWriteBuffer object with the reserved size. - */ - virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0; virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0; - - // Optimized method that pre-allocates buffer based on message size - bool send_message_(const ProtoMessage &msg, uint8_t message_type) { - ProtoSize size; - msg.calculate_size(size); - uint32_t msg_size = size.get_size(); - - // Create a pre-sized buffer - auto buffer = this->create_buffer(msg_size); - - // Encode message into the buffer - msg.encode(buffer); - - // Send the buffer - return this->send_buffer(buffer, message_type); - } + /** + * Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending. + * This is the implementation method - callers should use send_message() which adds logging. + * @param msg The protobuf message to send. + * @param message_type The message type identifier. + * @return True if the message was sent successfully, false otherwise. + */ + virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0; // Authentication helper methods inline bool check_connection_setup_() { diff --git a/esphome/components/aqi/aqi_sensor.h b/esphome/components/aqi/aqi_sensor.h index a990f815fe..2e526ca825 100644 --- a/esphome/components/aqi/aqi_sensor.h +++ b/esphome/components/aqi/aqi_sensor.h @@ -10,7 +10,6 @@ class AQISensor : public sensor::Sensor, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; } void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; } diff --git a/esphome/components/as3935/as3935.cpp b/esphome/components/as3935/as3935.cpp index 93a0bff5b3..dd0ab714f7 100644 --- a/esphome/components/as3935/as3935.cpp +++ b/esphome/components/as3935/as3935.cpp @@ -41,8 +41,6 @@ void AS3935Component::dump_config() { #endif } -float AS3935Component::get_setup_priority() const { return setup_priority::DATA; } - void AS3935Component::loop() { if (!this->irq_pin_->digital_read()) return; diff --git a/esphome/components/as3935/as3935.h b/esphome/components/as3935/as3935.h index dc590c268e..5dff1cb0ae 100644 --- a/esphome/components/as3935/as3935.h +++ b/esphome/components/as3935/as3935.h @@ -74,7 +74,6 @@ class AS3935Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; } diff --git a/esphome/components/as5600/sensor/as5600_sensor.cpp b/esphome/components/as5600/sensor/as5600_sensor.cpp index feb8f6cebf..1c0f4bad2c 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.cpp +++ b/esphome/components/as5600/sensor/as5600_sensor.cpp @@ -22,8 +22,6 @@ static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R -float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; } - void AS5600Sensor::dump_config() { LOG_SENSOR("", "AS5600 Sensor", this); ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_); diff --git a/esphome/components/as5600/sensor/as5600_sensor.h b/esphome/components/as5600/sensor/as5600_sensor.h index 0af9b01ae5..d471be49b5 100644 --- a/esphome/components/as5600/sensor/as5600_sensor.h +++ b/esphome/components/as5600/sensor/as5600_sensor.h @@ -14,7 +14,6 @@ class AS5600Sensor : public PollingComponent, public Parented, public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; } void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; } diff --git a/esphome/components/as7341/as7341.cpp b/esphome/components/as7341/as7341.cpp index 893eaa850f..1e78d814c8 100644 --- a/esphome/components/as7341/as7341.cpp +++ b/esphome/components/as7341/as7341.cpp @@ -58,8 +58,6 @@ void AS7341Component::dump_config() { LOG_SENSOR(" ", "NIR", this->nir_); } -float AS7341Component::get_setup_priority() const { return setup_priority::DATA; } - void AS7341Component::update() { this->read_channels(this->channel_readings_); diff --git a/esphome/components/as7341/as7341.h b/esphome/components/as7341/as7341.h index aed7996cef..3ede9d4aa4 100644 --- a/esphome/components/as7341/as7341.h +++ b/esphome/components/as7341/as7341.h @@ -78,7 +78,6 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; } diff --git a/esphome/components/atm90e26/atm90e26.cpp b/esphome/components/atm90e26/atm90e26.cpp index cadc06ac6b..2203dd0d71 100644 --- a/esphome/components/atm90e26/atm90e26.cpp +++ b/esphome/components/atm90e26/atm90e26.cpp @@ -146,7 +146,6 @@ void ATM90E26Component::dump_config() { LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_); } -float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; } uint16_t ATM90E26Component::read16_(uint8_t a_register) { uint8_t data[2]; diff --git a/esphome/components/atm90e26/atm90e26.h b/esphome/components/atm90e26/atm90e26.h index 3c098d7e91..d15a53ea43 100644 --- a/esphome/components/atm90e26/atm90e26.h +++ b/esphome/components/atm90e26/atm90e26.h @@ -13,7 +13,6 @@ class ATM90E26Component : public PollingComponent, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; } diff --git a/esphome/components/audio/__init__.py b/esphome/components/audio/__init__.py index 6c721652e1..f48b776ddd 100644 --- a/esphome/components/audio/__init__.py +++ b/esphome/components/audio/__init__.py @@ -1,5 +1,5 @@ import esphome.codegen as cg -from esphome.components.esp32 import add_idf_component +from esphome.components.esp32 import add_idf_component, include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE import esphome.final_validate as fv @@ -166,6 +166,9 @@ def final_validate_audio_schema( async def to_code(config): + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + include_builtin_idf_component("esp_http_client") + add_idf_component( name="esphome/esp-audio-libs", ref="2.0.3", diff --git a/esphome/components/bang_bang/bang_bang_climate.cpp b/esphome/components/bang_bang/bang_bang_climate.cpp index f26377a38a..6871e9df5d 100644 --- a/esphome/components/bang_bang/bang_bang_climate.cpp +++ b/esphome/components/bang_bang/bang_bang_climate.cpp @@ -6,8 +6,7 @@ namespace bang_bang { static const char *const TAG = "bang_bang.climate"; -BangBangClimate::BangBangClimate() - : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {} +BangBangClimate::BangBangClimate() = default; void BangBangClimate::setup() { this->sensor_->add_on_state_callback([this](float state) { @@ -160,13 +159,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) { switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: - trig = this->idle_trigger_; + trig = &this->idle_trigger_; break; case climate::CLIMATE_ACTION_COOLING: - trig = this->cool_trigger_; + trig = &this->cool_trigger_; break; case climate::CLIMATE_ACTION_HEATING: - trig = this->heat_trigger_; + trig = &this->heat_trigger_; break; default: trig = nullptr; @@ -204,9 +203,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; } void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } -Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; } -Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; } -Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; } +Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; } +Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; } +Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; } void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; } void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; } diff --git a/esphome/components/bang_bang/bang_bang_climate.h b/esphome/components/bang_bang/bang_bang_climate.h index 2e7da93a07..d0ddef2848 100644 --- a/esphome/components/bang_bang/bang_bang_climate.h +++ b/esphome/components/bang_bang/bang_bang_climate.h @@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component { void set_normal_config(const BangBangClimateTargetTempConfig &normal_config); void set_away_config(const BangBangClimateTargetTempConfig &away_config); - Trigger<> *get_idle_trigger() const; - Trigger<> *get_cool_trigger() const; - Trigger<> *get_heat_trigger() const; + Trigger<> *get_idle_trigger(); + Trigger<> *get_cool_trigger(); + Trigger<> *get_heat_trigger(); protected: /// Override control to change settings of the climate device. @@ -57,17 +57,13 @@ class BangBangClimate : public climate::Climate, public Component { * * In idle mode, the controller is assumed to have both heating and cooling disabled. */ - Trigger<> *idle_trigger_{nullptr}; + Trigger<> idle_trigger_; /** The trigger to call when the controller should switch to cooling mode. */ - Trigger<> *cool_trigger_{nullptr}; + Trigger<> cool_trigger_; /** The trigger to call when the controller should switch to heating mode. - * - * A null value for this attribute means that the controller has no heating action - * For example window blinds, where only cooling (blinds closed) and not-cooling - * (blinds open) is possible. */ - Trigger<> *heat_trigger_{nullptr}; + Trigger<> heat_trigger_; /** A reference to the trigger that was previously active. * * This is so that the previous trigger can be stopped before enabling a new one. diff --git a/esphome/components/bh1750/bh1750.cpp b/esphome/components/bh1750/bh1750.cpp index bd7c667c25..045fb7cf45 100644 --- a/esphome/components/bh1750/bh1750.cpp +++ b/esphome/components/bh1750/bh1750.cpp @@ -265,6 +265,4 @@ void BH1750Sensor::fail_and_reset_() { this->state_ = IDLE; } -float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace esphome::bh1750 diff --git a/esphome/components/bh1750/bh1750.h b/esphome/components/bh1750/bh1750.h index 0460427954..39dbd1d6a9 100644 --- a/esphome/components/bh1750/bh1750.h +++ b/esphome/components/bh1750/bh1750.h @@ -21,7 +21,6 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c: void dump_config() override; void update() override; void loop() override; - float get_setup_priority() const override; protected: // State machine states diff --git a/esphome/components/bme280_base/bme280_base.cpp b/esphome/components/bme280_base/bme280_base.cpp index c5d4c9c0a5..f396888fd1 100644 --- a/esphome/components/bme280_base/bme280_base.cpp +++ b/esphome/components/bme280_base/bme280_base.cpp @@ -199,7 +199,6 @@ void BME280Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_)); } -float BME280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bme280_base/bme280_base.h b/esphome/components/bme280_base/bme280_base.h index 0f55ad0101..00781d05b2 100644 --- a/esphome/components/bme280_base/bme280_base.h +++ b/esphome/components/bme280_base/bme280_base.h @@ -76,7 +76,6 @@ class BME280Component : public PollingComponent { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680/bme680.cpp b/esphome/components/bme680/bme680.cpp index 16435ccfee..5e52c84b3d 100644 --- a/esphome/components/bme680/bme680.cpp +++ b/esphome/components/bme680/bme680.cpp @@ -233,8 +233,6 @@ void BME680Component::dump_config() { } } -float BME680Component::get_setup_priority() const { return setup_priority::DATA; } - void BME680Component::update() { uint8_t meas_control = 0; // No need to fetch, we're setting all fields meas_control |= (this->temperature_oversampling_ & 0b111) << 5; diff --git a/esphome/components/bme680/bme680.h b/esphome/components/bme680/bme680.h index cfa7aaca20..d48a42823b 100644 --- a/esphome/components/bme680/bme680.h +++ b/esphome/components/bme680/bme680.h @@ -99,7 +99,6 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice { // (In most use cases you won't need these) void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bme680_bsec/__init__.py b/esphome/components/bme680_bsec/__init__.py index 06e641d34d..a86e061cd4 100644 --- a/esphome/components/bme680_bsec/__init__.py +++ b/esphome/components/bme680_bsec/__init__.py @@ -89,8 +89,9 @@ async def to_code(config): var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds) ) - # Although this component does not use SPI, the BSEC library requires the SPI library + # Although this component does not use SPI/Wire directly, the BSEC library requires them cg.add_library("SPI", None) + cg.add_library("Wire", None) cg.add_define("USE_BSEC") cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480") diff --git a/esphome/components/bme680_bsec/bme680_bsec.cpp b/esphome/components/bme680_bsec/bme680_bsec.cpp index d969c8fd98..392d071b31 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.cpp +++ b/esphome/components/bme680_bsec/bme680_bsec.cpp @@ -181,8 +181,6 @@ void BME680BSECComponent::dump_config() { LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_); } -float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; } - void BME680BSECComponent::loop() { this->run_(); diff --git a/esphome/components/bme680_bsec/bme680_bsec.h b/esphome/components/bme680_bsec/bme680_bsec.h index e52dbe964b..ec919f31df 100644 --- a/esphome/components/bme680_bsec/bme680_bsec.h +++ b/esphome/components/bme680_bsec/bme680_bsec.h @@ -64,7 +64,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp index 91383c8d45..1a42c9d54b 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.cpp @@ -106,8 +106,6 @@ void BME68xBSEC2Component::dump_config() { #endif } -float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; } - void BME68xBSEC2Component::loop() { this->run_(); diff --git a/esphome/components/bme68x_bsec2/bme68x_bsec2.h b/esphome/components/bme68x_bsec2/bme68x_bsec2.h index 86d3e5dfbf..8f4d8f61c2 100644 --- a/esphome/components/bme68x_bsec2/bme68x_bsec2.h +++ b/esphome/components/bme68x_bsec2/bme68x_bsec2.h @@ -48,7 +48,6 @@ class BME68xBSEC2Component : public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; } diff --git a/esphome/components/bmi160/bmi160.cpp b/esphome/components/bmi160/bmi160.cpp index 4fcc3edb82..1e8c91d7b7 100644 --- a/esphome/components/bmi160/bmi160.cpp +++ b/esphome/components/bmi160/bmi160.cpp @@ -263,7 +263,6 @@ void BMI160Component::update() { this->status_clear_warning(); } -float BMI160Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmi160 } // namespace esphome diff --git a/esphome/components/bmi160/bmi160.h b/esphome/components/bmi160/bmi160.h index 47691a4de9..16cab69733 100644 --- a/esphome/components/bmi160/bmi160.h +++ b/esphome/components/bmi160/bmi160.h @@ -14,8 +14,6 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/bmp085/bmp085.cpp b/esphome/components/bmp085/bmp085.cpp index 657da34f9b..9a383b2654 100644 --- a/esphome/components/bmp085/bmp085.cpp +++ b/esphome/components/bmp085/bmp085.cpp @@ -131,7 +131,6 @@ bool BMP085Component::set_mode_(uint8_t mode) { ESP_LOGV(TAG, "Setting mode to 0x%02X", mode); return this->write_byte(BMP085_REGISTER_CONTROL, mode); } -float BMP085Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace bmp085 } // namespace esphome diff --git a/esphome/components/bmp085/bmp085.h b/esphome/components/bmp085/bmp085.h index d84b4d43ef..c7315827e0 100644 --- a/esphome/components/bmp085/bmp085.h +++ b/esphome/components/bmp085/bmp085.h @@ -18,8 +18,6 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: struct CalibrationData { int16_t ac1, ac2, ac3; diff --git a/esphome/components/bmp280_base/bmp280_base.cpp b/esphome/components/bmp280_base/bmp280_base.cpp index 728eead521..de685e7c27 100644 --- a/esphome/components/bmp280_base/bmp280_base.cpp +++ b/esphome/components/bmp280_base/bmp280_base.cpp @@ -148,7 +148,6 @@ void BMP280Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_)); } -float BMP280Component::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; } diff --git a/esphome/components/bmp280_base/bmp280_base.h b/esphome/components/bmp280_base/bmp280_base.h index a47a794e96..836eafaf8b 100644 --- a/esphome/components/bmp280_base/bmp280_base.h +++ b/esphome/components/bmp280_base/bmp280_base.h @@ -64,7 +64,6 @@ class BMP280Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.cpp b/esphome/components/bmp3xx_base/bmp3xx_base.cpp index acc28d4e85..d7d9972170 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.cpp +++ b/esphome/components/bmp3xx_base/bmp3xx_base.cpp @@ -179,7 +179,6 @@ void BMP3XXComponent::dump_config() { ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_))); } } -float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; } inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); } diff --git a/esphome/components/bmp3xx_base/bmp3xx_base.h b/esphome/components/bmp3xx_base/bmp3xx_base.h index 50f92e04c1..8d2312231b 100644 --- a/esphome/components/bmp3xx_base/bmp3xx_base.h +++ b/esphome/components/bmp3xx_base/bmp3xx_base.h @@ -73,7 +73,6 @@ class BMP3XXComponent : public PollingComponent { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/cc1101/cc1101.cpp b/esphome/components/cc1101/cc1101.cpp index 46cd89e0e8..b6973da78d 100644 --- a/esphome/components/cc1101/cc1101.cpp +++ b/esphome/components/cc1101/cc1101.cpp @@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector &packet, float for (auto &listener : this->listeners_) { listener->on_packet(packet, freq_offset, rssi, lqi); } - this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi); + this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi); } void CC1101Component::loop() { diff --git a/esphome/components/cc1101/cc1101.h b/esphome/components/cc1101/cc1101.h index 6e3f01af90..e55071e7e3 100644 --- a/esphome/components/cc1101/cc1101.h +++ b/esphome/components/cc1101/cc1101.h @@ -79,7 +79,7 @@ class CC1101Component : public Component, // Packet mode operations CC1101Error transmit_packet(const std::vector &packet); void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; } + Trigger, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; } protected: uint16_t chip_id_{0}; @@ -96,8 +96,7 @@ class CC1101Component : public Component, // Packet handling void call_listeners_(const std::vector &packet, float freq_offset, float rssi, uint8_t lqi); - Trigger, float, float, uint8_t> *packet_trigger_{ - new Trigger, float, float, uint8_t>()}; + Trigger, float, float, uint8_t> packet_trigger_; std::vector packet_; std::vector listeners_; diff --git a/esphome/components/cd74hc4067/cd74hc4067.cpp b/esphome/components/cd74hc4067/cd74hc4067.cpp index 4293d7af07..302c83d7d3 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.cpp +++ b/esphome/components/cd74hc4067/cd74hc4067.cpp @@ -7,8 +7,6 @@ namespace cd74hc4067 { static const char *const TAG = "cd74hc4067"; -float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; } - void CD74HC4067Component::setup() { this->pin_s0_->setup(); this->pin_s1_->setup(); diff --git a/esphome/components/cd74hc4067/cd74hc4067.h b/esphome/components/cd74hc4067/cd74hc4067.h index 6193513575..76ebc1ebbe 100644 --- a/esphome/components/cd74hc4067/cd74hc4067.h +++ b/esphome/components/cd74hc4067/cd74hc4067.h @@ -13,7 +13,6 @@ class CD74HC4067Component : public Component { /// Set up the internal sensor array. void setup() override; void dump_config() override; - float get_setup_priority() const override; /// setting pin active by setting the right combination of the four multiplexer input pins void activate_pin(uint8_t pin); diff --git a/esphome/components/ch423/__init__.py b/esphome/components/ch423/__init__.py new file mode 100644 index 0000000000..e3990ee631 --- /dev/null +++ b/esphome/components/ch423/__init__.py @@ -0,0 +1,103 @@ +from esphome import pins +import esphome.codegen as cg +from esphome.components import i2c +from esphome.components.i2c import I2CBus +import esphome.config_validation as cv +from esphome.const import ( + CONF_I2C_ID, + CONF_ID, + CONF_INPUT, + CONF_INVERTED, + CONF_MODE, + CONF_NUMBER, + CONF_OPEN_DRAIN, + CONF_OUTPUT, +) +from esphome.core import CORE + +CODEOWNERS = ["@dwmw2"] +DEPENDENCIES = ["i2c"] +MULTI_CONF = True +ch423_ns = cg.esphome_ns.namespace("ch423") + +CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice) +CH423GPIOPin = ch423_ns.class_( + "CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component) +) + +CONF_CH423 = "ch423" + +# Note that no address is configurable - each register in the CH423 has a dedicated i2c address +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_ID): cv.declare_id(CH423Component), + cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + # Can't use register_i2c_device because there is no CONF_ADDRESS + parent = await cg.get_variable(config[CONF_I2C_ID]) + cg.add(var.set_i2c_bus(parent)) + + +# This is used as a final validation step so that modes have been fully transformed. +def pin_mode_check(pin_config, _): + if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8: + raise cv.Invalid("CH423 only supports input on pins 0-7") + if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8: + raise cv.Invalid("CH423 only supports open drain output on pins 8-23") + + ch423_id = pin_config[CONF_CH423] + pin_num = pin_config[CONF_NUMBER] + is_output = pin_config[CONF_MODE][CONF_OUTPUT] + is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN] + + # Track pin modes per CH423 instance in CORE.data + ch423_modes = CORE.data.setdefault(CONF_CH423, {}) + if ch423_id not in ch423_modes: + ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None} + + if pin_num < 8: + # GPIO pins (0-7): all must have same direction + if ch423_modes[ch423_id]["gpio_output"] is None: + ch423_modes[ch423_id]["gpio_output"] = is_output + elif ch423_modes[ch423_id]["gpio_output"] != is_output: + raise cv.Invalid( + "CH423 GPIO pins (0-7) must all be configured as input or all as output" + ) + # GPO pins (8-23): all must have same open-drain setting + elif ch423_modes[ch423_id]["gpo_open_drain"] is None: + ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain + elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain: + raise cv.Invalid( + "CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain" + ) + + +CH423_PIN_SCHEMA = pins.gpio_base_schema( + CH423GPIOPin, + cv.int_range(min=0, max=23), + modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN], +).extend( + { + cv.Required(CONF_CH423): cv.use_id(CH423Component), + } +) + + +@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check) +async def ch423_pin_to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + parent = await cg.get_variable(config[CONF_CH423]) + + cg.add(var.set_parent(parent)) + + num = config[CONF_NUMBER] + cg.add(var.set_pin(num)) + cg.add(var.set_inverted(config[CONF_INVERTED])) + cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) + return var diff --git a/esphome/components/ch423/ch423.cpp b/esphome/components/ch423/ch423.cpp new file mode 100644 index 0000000000..4abbbe7adf --- /dev/null +++ b/esphome/components/ch423/ch423.cpp @@ -0,0 +1,148 @@ +#include "ch423.h" +#include "esphome/core/log.h" +#include "esphome/core/progmem.h" + +namespace esphome::ch423 { + +static constexpr uint8_t CH423_REG_SYS = 0x24; // Set system parameters (0x48 >> 1) +static constexpr uint8_t CH423_SYS_IO_OE = 0x01; // IO output enable +static constexpr uint8_t CH423_SYS_OD_EN = 0x04; // Open drain enable for OC pins +static constexpr uint8_t CH423_REG_IO = 0x30; // Write/read IO7-IO0 (0x60 >> 1) +static constexpr uint8_t CH423_REG_IO_RD = 0x26; // Read IO7-IO0 (0x4D >> 1, rounded down) +static constexpr uint8_t CH423_REG_OCL = 0x22; // Write OC7-OC0 (0x44 >> 1) +static constexpr uint8_t CH423_REG_OCH = 0x23; // Write OC15-OC8 (0x46 >> 1) + +static const char *const TAG = "ch423"; + +void CH423Component::setup() { + // set outputs before mode + this->write_outputs_(); + // Set system parameters and check for errors + bool success = this->write_reg_(CH423_REG_SYS, this->sys_params_); + // Only read inputs if pins are configured for input (IO_OE not set) + if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) { + success = this->read_inputs_(); + } + if (!success) { + ESP_LOGE(TAG, "CH423 not detected"); + this->mark_failed(); + return; + } + + ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(), + this->status_has_error()); +} + +void CH423Component::loop() { + // Clear all the previously read flags. + this->pin_read_flags_ = 0x00; +} + +void CH423Component::dump_config() { + ESP_LOGCONFIG(TAG, "CH423:"); + if (this->is_failed()) { + ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); + } +} + +void CH423Component::pin_mode(uint8_t pin, gpio::Flags flags) { + if (pin < 8) { + if (flags & gpio::FLAG_OUTPUT) { + this->sys_params_ |= CH423_SYS_IO_OE; + } + } else if (pin >= 8 && pin < 24) { + if (flags & gpio::FLAG_OPEN_DRAIN) { + this->sys_params_ |= CH423_SYS_OD_EN; + } + } +} + +bool CH423Component::digital_read(uint8_t pin) { + if (this->pin_read_flags_ == 0 || this->pin_read_flags_ & (1 << pin)) { + // Read values on first access or in case it's being read again in the same loop + this->read_inputs_(); + } + + this->pin_read_flags_ |= (1 << pin); + return (this->input_bits_ & (1 << pin)) != 0; +} + +void CH423Component::digital_write(uint8_t pin, bool value) { + if (value) { + this->output_bits_ |= (1 << pin); + } else { + this->output_bits_ &= ~(1 << pin); + } + this->write_outputs_(); +} + +bool CH423Component::read_inputs_() { + if (this->is_failed()) { + return false; + } + // reading inputs requires IO_OE to be 0 + if (this->sys_params_ & CH423_SYS_IO_OE) { + return false; + } + uint8_t result = this->read_reg_(CH423_REG_IO_RD); + this->input_bits_ = result; + this->status_clear_warning(); + return true; +} + +// Write a register. Can't use the standard write_byte() method because there is no single pre-configured i2c address. +bool CH423Component::write_reg_(uint8_t reg, uint8_t value) { + auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0); + if (err != i2c::ERROR_OK) { + char buf[64]; + ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("write failed for register 0x%X, error %d"), reg, err); + this->status_set_warning(buf); + return false; + } + this->status_clear_warning(); + return true; +} + +uint8_t CH423Component::read_reg_(uint8_t reg) { + uint8_t value; + auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1); + if (err != i2c::ERROR_OK) { + char buf[64]; + ESPHOME_snprintf_P(buf, sizeof(buf), ESPHOME_PSTR("read failed for register 0x%X, error %d"), reg, err); + this->status_set_warning(buf); + return 0; + } + this->status_clear_warning(); + return value; +} + +bool CH423Component::write_outputs_() { + bool success = true; + // Write IO7-IO0 + success &= this->write_reg_(CH423_REG_IO, static_cast(this->output_bits_)); + // Write OC7-OC0 + success &= this->write_reg_(CH423_REG_OCL, static_cast(this->output_bits_ >> 8)); + // Write OC15-OC8 + success &= this->write_reg_(CH423_REG_OCH, static_cast(this->output_bits_ >> 16)); + return success; +} + +float CH423Component::get_setup_priority() const { return setup_priority::IO; } + +// Run our loop() method very early in the loop, so that we cache read values +// before other components call our digital_read() method. +float CH423Component::get_loop_priority() const { return 9.0f; } // Just after WIFI + +void CH423GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } +bool CH423GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; } + +void CH423GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } +size_t CH423GPIOPin::dump_summary(char *buffer, size_t len) const { + return snprintf(buffer, len, "EXIO%u via CH423", this->pin_); +} +void CH423GPIOPin::set_flags(gpio::Flags flags) { + flags_ = flags; + this->parent_->pin_mode(this->pin_, flags); +} + +} // namespace esphome::ch423 diff --git a/esphome/components/ch423/ch423.h b/esphome/components/ch423/ch423.h new file mode 100644 index 0000000000..7adc7de6a1 --- /dev/null +++ b/esphome/components/ch423/ch423.h @@ -0,0 +1,67 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/hal.h" +#include "esphome/components/i2c/i2c.h" + +namespace esphome::ch423 { + +class CH423Component : public Component, public i2c::I2CDevice { + public: + CH423Component() = default; + + /// Check i2c availability and setup masks + void setup() override; + /// Poll for input changes periodically + void loop() override; + /// Helper function to read the value of a pin. + bool digital_read(uint8_t pin); + /// Helper function to write the value of a pin. + void digital_write(uint8_t pin, bool value); + /// Helper function to set the pin mode of a pin. + void pin_mode(uint8_t pin, gpio::Flags flags); + + float get_setup_priority() const override; + float get_loop_priority() const override; + void dump_config() override; + + protected: + bool write_reg_(uint8_t reg, uint8_t value); + uint8_t read_reg_(uint8_t reg); + bool read_inputs_(); + bool write_outputs_(); + + /// The mask to write as output state - 1 means HIGH, 0 means LOW + uint32_t output_bits_{0x00}; + /// Flags to check if read previously during this loop + uint8_t pin_read_flags_{0x00}; + /// Copy of last read values + uint8_t input_bits_{0x00}; + /// System parameters + uint8_t sys_params_{0x00}; +}; + +/// Helper class to expose a CH423 pin as a GPIO pin. +class CH423GPIOPin : public GPIOPin { + public: + void setup() override{}; + void pin_mode(gpio::Flags flags) override; + bool digital_read() override; + void digital_write(bool value) override; + size_t dump_summary(char *buffer, size_t len) const override; + + void set_parent(CH423Component *parent) { parent_ = parent; } + void set_pin(uint8_t pin) { pin_ = pin; } + void set_inverted(bool inverted) { inverted_ = inverted; } + void set_flags(gpio::Flags flags); + + gpio::Flags get_flags() const override { return this->flags_; } + + protected: + CH423Component *parent_{}; + uint8_t pin_{}; + bool inverted_{}; + gpio::Flags flags_{}; +}; + +} // namespace esphome::ch423 diff --git a/esphome/components/climate/climate_mode.cpp b/esphome/components/climate/climate_mode.cpp index b153ee0424..c4dd19d503 100644 --- a/esphome/components/climate/climate_mode.cpp +++ b/esphome/components/climate/climate_mode.cpp @@ -1,109 +1,44 @@ #include "climate_mode.h" +#include "esphome/core/progmem.h" namespace esphome::climate { +// Climate mode strings indexed by ClimateMode enum (0-6): OFF, HEAT_COOL, COOL, HEAT, FAN_ONLY, DRY, AUTO +PROGMEM_STRING_TABLE(ClimateModeStrings, "OFF", "HEAT_COOL", "COOL", "HEAT", "FAN_ONLY", "DRY", "AUTO", "UNKNOWN"); + const LogString *climate_mode_to_string(ClimateMode mode) { - switch (mode) { - case CLIMATE_MODE_OFF: - return LOG_STR("OFF"); - case CLIMATE_MODE_HEAT_COOL: - return LOG_STR("HEAT_COOL"); - case CLIMATE_MODE_AUTO: - return LOG_STR("AUTO"); - case CLIMATE_MODE_COOL: - return LOG_STR("COOL"); - case CLIMATE_MODE_HEAT: - return LOG_STR("HEAT"); - case CLIMATE_MODE_FAN_ONLY: - return LOG_STR("FAN_ONLY"); - case CLIMATE_MODE_DRY: - return LOG_STR("DRY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateModeStrings::get_log_str(static_cast(mode), ClimateModeStrings::LAST_INDEX); } + +// Climate action strings indexed by ClimateAction enum (0,2-6): OFF, (gap), COOLING, HEATING, IDLE, DRYING, FAN +PROGMEM_STRING_TABLE(ClimateActionStrings, "OFF", "UNKNOWN", "COOLING", "HEATING", "IDLE", "DRYING", "FAN", "UNKNOWN"); + const LogString *climate_action_to_string(ClimateAction action) { - switch (action) { - case CLIMATE_ACTION_OFF: - return LOG_STR("OFF"); - case CLIMATE_ACTION_COOLING: - return LOG_STR("COOLING"); - case CLIMATE_ACTION_HEATING: - return LOG_STR("HEATING"); - case CLIMATE_ACTION_IDLE: - return LOG_STR("IDLE"); - case CLIMATE_ACTION_DRYING: - return LOG_STR("DRYING"); - case CLIMATE_ACTION_FAN: - return LOG_STR("FAN"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateActionStrings::get_log_str(static_cast(action), ClimateActionStrings::LAST_INDEX); } +// Climate fan mode strings indexed by ClimateFanMode enum (0-9): ON, OFF, AUTO, LOW, MEDIUM, HIGH, MIDDLE, FOCUS, +// DIFFUSE, QUIET +PROGMEM_STRING_TABLE(ClimateFanModeStrings, "ON", "OFF", "AUTO", "LOW", "MEDIUM", "HIGH", "MIDDLE", "FOCUS", "DIFFUSE", + "QUIET", "UNKNOWN"); + const LogString *climate_fan_mode_to_string(ClimateFanMode fan_mode) { - switch (fan_mode) { - case climate::CLIMATE_FAN_ON: - return LOG_STR("ON"); - case climate::CLIMATE_FAN_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_FAN_AUTO: - return LOG_STR("AUTO"); - case climate::CLIMATE_FAN_LOW: - return LOG_STR("LOW"); - case climate::CLIMATE_FAN_MEDIUM: - return LOG_STR("MEDIUM"); - case climate::CLIMATE_FAN_HIGH: - return LOG_STR("HIGH"); - case climate::CLIMATE_FAN_MIDDLE: - return LOG_STR("MIDDLE"); - case climate::CLIMATE_FAN_FOCUS: - return LOG_STR("FOCUS"); - case climate::CLIMATE_FAN_DIFFUSE: - return LOG_STR("DIFFUSE"); - case climate::CLIMATE_FAN_QUIET: - return LOG_STR("QUIET"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateFanModeStrings::get_log_str(static_cast(fan_mode), ClimateFanModeStrings::LAST_INDEX); } +// Climate swing mode strings indexed by ClimateSwingMode enum (0-3): OFF, BOTH, VERTICAL, HORIZONTAL +PROGMEM_STRING_TABLE(ClimateSwingModeStrings, "OFF", "BOTH", "VERTICAL", "HORIZONTAL", "UNKNOWN"); + const LogString *climate_swing_mode_to_string(ClimateSwingMode swing_mode) { - switch (swing_mode) { - case climate::CLIMATE_SWING_OFF: - return LOG_STR("OFF"); - case climate::CLIMATE_SWING_BOTH: - return LOG_STR("BOTH"); - case climate::CLIMATE_SWING_VERTICAL: - return LOG_STR("VERTICAL"); - case climate::CLIMATE_SWING_HORIZONTAL: - return LOG_STR("HORIZONTAL"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimateSwingModeStrings::get_log_str(static_cast(swing_mode), ClimateSwingModeStrings::LAST_INDEX); } +// Climate preset strings indexed by ClimatePreset enum (0-7): NONE, HOME, AWAY, BOOST, COMFORT, ECO, SLEEP, ACTIVITY +PROGMEM_STRING_TABLE(ClimatePresetStrings, "NONE", "HOME", "AWAY", "BOOST", "COMFORT", "ECO", "SLEEP", "ACTIVITY", + "UNKNOWN"); + const LogString *climate_preset_to_string(ClimatePreset preset) { - switch (preset) { - case climate::CLIMATE_PRESET_NONE: - return LOG_STR("NONE"); - case climate::CLIMATE_PRESET_HOME: - return LOG_STR("HOME"); - case climate::CLIMATE_PRESET_ECO: - return LOG_STR("ECO"); - case climate::CLIMATE_PRESET_AWAY: - return LOG_STR("AWAY"); - case climate::CLIMATE_PRESET_BOOST: - return LOG_STR("BOOST"); - case climate::CLIMATE_PRESET_COMFORT: - return LOG_STR("COMFORT"); - case climate::CLIMATE_PRESET_SLEEP: - return LOG_STR("SLEEP"); - case climate::CLIMATE_PRESET_ACTIVITY: - return LOG_STR("ACTIVITY"); - default: - return LOG_STR("UNKNOWN"); - } + return ClimatePresetStrings::get_log_str(static_cast(preset), ClimatePresetStrings::LAST_INDEX); } } // namespace esphome::climate diff --git a/esphome/components/cm1106/cm1106.h b/esphome/components/cm1106/cm1106.h index ad089bbe7d..8c33e56457 100644 --- a/esphome/components/cm1106/cm1106.h +++ b/esphome/components/cm1106/cm1106.h @@ -10,8 +10,6 @@ namespace cm1106 { class CM1106Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/combination/combination.h b/esphome/components/combination/combination.h index 901aeaf259..fb5e156da9 100644 --- a/esphome/components/combination/combination.h +++ b/esphome/components/combination/combination.h @@ -10,8 +10,6 @@ namespace combination { class CombinationComponent : public Component, public sensor::Sensor { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - /// @brief Logs all source sensor's names virtual void log_source_sensors() = 0; diff --git a/esphome/components/const/__init__.py b/esphome/components/const/__init__.py index fcfafa0c1a..3201db5dfd 100644 --- a/esphome/components/const/__init__.py +++ b/esphome/components/const/__init__.py @@ -17,3 +17,9 @@ CONF_ON_STATE_CHANGE = "on_state_change" CONF_REQUEST_HEADERS = "request_headers" CONF_ROWS = "rows" CONF_USE_PSRAM = "use_psram" + +ICON_CURRENT_DC = "mdi:current-dc" +ICON_SOLAR_PANEL = "mdi:solar-panel" +ICON_SOLAR_POWER = "mdi:solar-power" + +UNIT_AMPERE_HOUR = "Ah" diff --git a/esphome/components/cover/__init__.py b/esphome/components/cover/__init__.py index 383daee083..41774f3d71 100644 --- a/esphome/components/cover/__init__.py +++ b/esphome/components/cover/__init__.py @@ -1,3 +1,5 @@ +import logging + from esphome import automation from esphome.automation import Condition, maybe_simple_id import esphome.codegen as cg @@ -9,6 +11,7 @@ from esphome.const import ( CONF_ICON, CONF_ID, CONF_MQTT_ID, + CONF_ON_IDLE, CONF_ON_OPEN, CONF_POSITION, CONF_POSITION_COMMAND_TOPIC, @@ -32,9 +35,10 @@ from esphome.const import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, ) -from esphome.core import CORE, CoroPriority, coroutine_with_priority +from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity -from esphome.cpp_generator import MockObjClass +from esphome.cpp_generator import MockObj, MockObjClass +from esphome.types import ConfigType, TemplateArgsType IS_PLATFORM_COMPONENT = True @@ -53,6 +57,8 @@ DEVICE_CLASSES = [ DEVICE_CLASS_WINDOW, ] +_LOGGER = logging.getLogger(__name__) + cover_ns = cg.esphome_ns.namespace("cover") Cover = cover_ns.class_("Cover", cg.EntityBase) @@ -83,14 +89,29 @@ ControlAction = cover_ns.class_("ControlAction", automation.Action) CoverPublishAction = cover_ns.class_("CoverPublishAction", automation.Action) CoverIsOpenCondition = cover_ns.class_("CoverIsOpenCondition", Condition) CoverIsClosedCondition = cover_ns.class_("CoverIsClosedCondition", Condition) - -# Triggers -CoverOpenTrigger = cover_ns.class_("CoverOpenTrigger", automation.Trigger.template()) +CoverOpenedTrigger = cover_ns.class_( + "CoverOpenedTrigger", automation.Trigger.template() +) CoverClosedTrigger = cover_ns.class_( "CoverClosedTrigger", automation.Trigger.template() ) +CoverTrigger = cover_ns.class_("CoverTrigger", automation.Trigger.template()) +# Cover-specific constants CONF_ON_CLOSED = "on_closed" +CONF_ON_OPENED = "on_opened" +CONF_ON_OPENING = "on_opening" +CONF_ON_CLOSING = "on_closing" + +TRIGGERS = { + CONF_ON_OPEN: CoverOpenedTrigger, # Deprecated, use on_opened + CONF_ON_OPENED: CoverOpenedTrigger, + CONF_ON_CLOSED: CoverClosedTrigger, + CONF_ON_CLOSING: CoverTrigger.template(CoverOperation.COVER_OPERATION_CLOSING), + CONF_ON_OPENING: CoverTrigger.template(CoverOperation.COVER_OPERATION_OPENING), + CONF_ON_IDLE: CoverTrigger.template(CoverOperation.COVER_OPERATION_IDLE), +} + _COVER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) @@ -111,16 +132,14 @@ _COVER_SCHEMA = ( cv.Optional(CONF_TILT_STATE_TOPIC): cv.All( cv.requires_component("mqtt"), cv.subscribe_topic ), - cv.Optional(CONF_ON_OPEN): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverOpenTrigger), - } - ), - cv.Optional(CONF_ON_CLOSED): automation.validate_automation( - { - cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CoverClosedTrigger), - } - ), + **{ + cv.Optional(conf): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(trigger_class), + } + ) + for conf, trigger_class in TRIGGERS.items() + }, } ) ) @@ -157,12 +176,14 @@ async def setup_cover_core_(var, config): if (device_class := config.get(CONF_DEVICE_CLASS)) is not None: cg.add(var.set_device_class(device_class)) - for conf in config.get(CONF_ON_OPEN, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) - for conf in config.get(CONF_ON_CLOSED, []): - trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) - await automation.build_automation(trigger, [], conf) + if CONF_ON_OPEN in config: + _LOGGER.warning( + "'on_open' is deprecated, use 'on_opened'. Will be removed in 2026.8.0" + ) + for trigger_conf in TRIGGERS: + for conf in config.get(trigger_conf, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) if (mqtt_id := config.get(CONF_MQTT_ID)) is not None: mqtt_ = cg.new_Pvariable(mqtt_id, var) @@ -258,6 +279,26 @@ async def cover_control_to_code(config, action_id, template_arg, args): return var +COVER_CONDITION_SCHEMA = cv.maybe_simple_value( + {cv.Required(CONF_ID): cv.use_id(Cover)}, key=CONF_ID +) + + +async def cover_condition_to_code( + config: ConfigType, condition_id: ID, template_arg: MockObj, args: TemplateArgsType +) -> MockObj: + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(condition_id, template_arg, paren) + + +automation.register_condition( + "cover.is_open", CoverIsOpenCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) +automation.register_condition( + "cover.is_closed", CoverIsClosedCondition, COVER_CONDITION_SCHEMA +)(cover_condition_to_code) + + @coroutine_with_priority(CoroPriority.CORE) async def to_code(config): cg.add_global(cover_ns.using) diff --git a/esphome/components/cover/automation.h b/esphome/components/cover/automation.h index c0345a7cc6..12ec46725d 100644 --- a/esphome/components/cover/automation.h +++ b/esphome/components/cover/automation.h @@ -90,44 +90,53 @@ template class CoverPublishAction : public Action { Cover *cover_; }; -template class CoverIsOpenCondition : public Condition { +template class CoverPositionCondition : public Condition { public: - CoverIsOpenCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_open(); } + CoverPositionCondition(Cover *cover) : cover_(cover) {} + + bool check(const Ts &...x) override { return this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED); } protected: Cover *cover_; }; -template class CoverIsClosedCondition : public Condition { +template using CoverIsOpenCondition = CoverPositionCondition; +template using CoverIsClosedCondition = CoverPositionCondition; + +template class CoverPositionTrigger : public Trigger<> { public: - CoverIsClosedCondition(Cover *cover) : cover_(cover) {} - bool check(const Ts &...x) override { return this->cover_->is_fully_closed(); } + CoverPositionTrigger(Cover *a_cover) { + a_cover->add_on_state_callback([this, a_cover]() { + if (a_cover->position != this->last_position_) { + this->last_position_ = a_cover->position; + if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED)) + this->trigger(); + } + }); + } protected: - Cover *cover_; + float last_position_{NAN}; }; -class CoverOpenTrigger : public Trigger<> { +using CoverOpenedTrigger = CoverPositionTrigger; +using CoverClosedTrigger = CoverPositionTrigger; + +template class CoverTrigger : public Trigger<> { public: - CoverOpenTrigger(Cover *a_cover) { + CoverTrigger(Cover *a_cover) { a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_open()) { - this->trigger(); + auto current_op = a_cover->current_operation; + if (current_op == OP) { + if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) { + this->trigger(); + } } + this->last_operation_ = current_op; }); } -}; -class CoverClosedTrigger : public Trigger<> { - public: - CoverClosedTrigger(Cover *a_cover) { - a_cover->add_on_state_callback([this, a_cover]() { - if (a_cover->is_fully_closed()) { - this->trigger(); - } - }); - } + protected: + optional last_operation_{}; }; - } // namespace esphome::cover diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index 0d9e7e8ffb..0589aa2379 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -10,9 +10,6 @@ namespace esphome::cover { static const char *const TAG = "cover"; -const float COVER_OPEN = 1.0f; -const float COVER_CLOSED = 0.0f; - const LogString *cover_command_to_str(float pos) { if (pos == COVER_OPEN) { return LOG_STR("OPEN"); @@ -22,17 +19,11 @@ const LogString *cover_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Cover operation strings indexed by CoverOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(CoverOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *cover_operation_to_str(CoverOperation op) { - switch (op) { - case COVER_OPERATION_IDLE: - return LOG_STR("IDLE"); - case COVER_OPERATION_OPENING: - return LOG_STR("OPENING"); - case COVER_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return CoverOperationStrings::get_log_str(static_cast(op), CoverOperationStrings::LAST_INDEX); } Cover::Cover() : position{COVER_OPEN} {} diff --git a/esphome/components/cover/cover.h b/esphome/components/cover/cover.h index e5427ceaa8..0af48f75de 100644 --- a/esphome/components/cover/cover.h +++ b/esphome/components/cover/cover.h @@ -10,8 +10,8 @@ namespace esphome::cover { -const extern float COVER_OPEN; -const extern float COVER_CLOSED; +static constexpr float COVER_OPEN = 1.0f; +static constexpr float COVER_CLOSED = 0.0f; #define LOG_COVER(prefix, type, obj) \ if ((obj) != nullptr) { \ diff --git a/esphome/components/cse7761/cse7761.cpp b/esphome/components/cse7761/cse7761.cpp index 482636dd81..7c5ee833a4 100644 --- a/esphome/components/cse7761/cse7761.cpp +++ b/esphome/components/cse7761/cse7761.cpp @@ -62,8 +62,6 @@ void CSE7761Component::dump_config() { this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8); } -float CSE7761Component::get_setup_priority() const { return setup_priority::DATA; } - void CSE7761Component::update() { if (this->data_.ready) { this->get_data_(); diff --git a/esphome/components/cse7761/cse7761.h b/esphome/components/cse7761/cse7761.h index 71846cdcab..289c5e7e19 100644 --- a/esphome/components/cse7761/cse7761.h +++ b/esphome/components/cse7761/cse7761.h @@ -28,7 +28,6 @@ class CSE7761Component : public PollingComponent, public uart::UARTDevice { void set_current_2_sensor(sensor::Sensor *current_sensor_2) { current_sensor_2_ = current_sensor_2; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 4432195365..df4872deac 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -37,7 +37,6 @@ void CSE7766Component::loop() { this->raw_data_index_ = (this->raw_data_index_ + 1) % 24; } } -float CSE7766Component::get_setup_priority() const { return setup_priority::DATA; } bool CSE7766Component::check_byte_() { uint8_t index = this->raw_data_index_; @@ -152,6 +151,10 @@ void CSE7766Component::parse_data_() { if (this->power_sensor_ != nullptr) { this->power_sensor_->publish_state(power); } + } else if (this->power_sensor_ != nullptr) { + // No valid power measurement from chip - publish 0W to avoid stale readings + // This typically happens when current is below the measurable threshold (~50mA) + this->power_sensor_->publish_state(0.0f); } float current = 0.0f; diff --git a/esphome/components/cse7766/cse7766.h b/esphome/components/cse7766/cse7766.h index 8902eafe3c..efddccd3c5 100644 --- a/esphome/components/cse7766/cse7766.h +++ b/esphome/components/cse7766/cse7766.h @@ -23,7 +23,6 @@ class CSE7766Component : public Component, public uart::UARTDevice { void set_power_factor_sensor(sensor::Sensor *power_factor_sensor) { power_factor_sensor_ = power_factor_sensor; } void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/current_based/current_based_cover.cpp b/esphome/components/current_based/current_based_cover.cpp index cb3f65c9cd..5dfaeeff39 100644 --- a/esphome/components/current_based/current_based_cover.cpp +++ b/esphome/components/current_based/current_based_cover.cpp @@ -66,7 +66,7 @@ void CurrentBasedCover::loop() { if (this->current_operation == COVER_OPERATION_OPENING) { if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during opening. Current flow detected in close circuit", this->name_.c_str()); } else if (this->is_opening_blocked_()) { // Blocked @@ -87,7 +87,7 @@ void CurrentBasedCover::loop() { } else if (this->current_operation == COVER_OPERATION_CLOSING) { if (this->malfunction_detection_ && this->is_opening_()) { // Malfunction this->direction_idle_(); - this->malfunction_trigger_->trigger(); + this->malfunction_trigger_.trigger(); ESP_LOGI(TAG, "'%s' - Malfunction detected during closing. Current flow detected in open circuit", this->name_.c_str()); } else if (this->is_closing_blocked_()) { // Blocked @@ -159,7 +159,6 @@ void CurrentBasedCover::dump_config() { this->start_sensing_delay_ / 1e3f, YESNO(this->malfunction_detection_)); } -float CurrentBasedCover::get_setup_priority() const { return setup_priority::DATA; } void CurrentBasedCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); @@ -221,15 +220,15 @@ void CurrentBasedCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/current_based/current_based_cover.h b/esphome/components/current_based/current_based_cover.h index b172e762b0..76bd85cdf7 100644 --- a/esphome/components/current_based/current_based_cover.h +++ b/esphome/components/current_based/current_based_cover.h @@ -14,11 +14,10 @@ class CurrentBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } - Trigger<> *get_open_trigger() const { return this->open_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; } void set_open_moving_current_threshold(float open_moving_current_threshold) { this->open_moving_current_threshold_ = open_moving_current_threshold; @@ -28,7 +27,7 @@ class CurrentBasedCover : public cover::Cover, public Component { } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; } void set_close_moving_current_threshold(float close_moving_current_threshold) { this->close_moving_current_threshold_ = close_moving_current_threshold; @@ -44,7 +43,7 @@ class CurrentBasedCover : public cover::Cover, public Component { void set_malfunction_detection(bool malfunction_detection) { this->malfunction_detection_ = malfunction_detection; } void set_start_sensing_delay(uint32_t start_sensing_delay) { this->start_sensing_delay_ = start_sensing_delay; } - Trigger<> *get_malfunction_trigger() const { return this->malfunction_trigger_; } + Trigger<> *get_malfunction_trigger() { return &this->malfunction_trigger_; } cover::CoverTraits get_traits() override; @@ -64,23 +63,23 @@ class CurrentBasedCover : public cover::Cover, public Component { void recompute_position_(); - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; sensor::Sensor *open_sensor_{nullptr}; - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; float open_moving_current_threshold_; float open_obstacle_current_threshold_{FLT_MAX}; uint32_t open_duration_; sensor::Sensor *close_sensor_{nullptr}; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; float close_moving_current_threshold_; float close_obstacle_current_threshold_{FLT_MAX}; uint32_t close_duration_; uint32_t max_duration_{UINT32_MAX}; bool malfunction_detection_{true}; - Trigger<> *malfunction_trigger_{new Trigger<>()}; + Trigger<> malfunction_trigger_; uint32_t start_sensing_delay_; float obstacle_rollback_; diff --git a/esphome/components/daly_bms/daly_bms.cpp b/esphome/components/daly_bms/daly_bms.cpp index 2d270cc56e..90ccee78f8 100644 --- a/esphome/components/daly_bms/daly_bms.cpp +++ b/esphome/components/daly_bms/daly_bms.cpp @@ -104,8 +104,6 @@ void DalyBmsComponent::loop() { } } -float DalyBmsComponent::get_setup_priority() const { return setup_priority::DATA; } - void DalyBmsComponent::request_data_(uint8_t data_id) { uint8_t request_message[DALY_FRAME_SIZE]; diff --git a/esphome/components/daly_bms/daly_bms.h b/esphome/components/daly_bms/daly_bms.h index e6d476bcdd..1983ba0ef1 100644 --- a/esphome/components/daly_bms/daly_bms.h +++ b/esphome/components/daly_bms/daly_bms.h @@ -72,7 +72,6 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { void update() override; void loop() override; - float get_setup_priority() const override; void set_address(uint8_t address) { this->addr_ = address; } protected: diff --git a/esphome/components/daly_bms/sensor.py b/esphome/components/daly_bms/sensor.py index 560bcef64e..aa92cfa86a 100644 --- a/esphome/components/daly_bms/sensor.py +++ b/esphome/components/daly_bms/sensor.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_LEVEL, @@ -55,14 +56,11 @@ CONF_CELL_15_VOLTAGE = "cell_15_voltage" CONF_CELL_16_VOLTAGE = "cell_16_voltage" CONF_CELL_17_VOLTAGE = "cell_17_voltage" CONF_CELL_18_VOLTAGE = "cell_18_voltage" -ICON_CURRENT_DC = "mdi:current-dc" ICON_BATTERY_OUTLINE = "mdi:battery-outline" ICON_THERMOMETER_CHEVRON_UP = "mdi:thermometer-chevron-up" ICON_THERMOMETER_CHEVRON_DOWN = "mdi:thermometer-chevron-down" ICON_CAR_BATTERY = "mdi:car-battery" -UNIT_AMPERE_HOUR = "Ah" - TYPES = [ CONF_VOLTAGE, CONF_CURRENT, diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 276ea24717..fef247f168 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -63,8 +63,6 @@ void DHT::update() { } } -float DHT::get_setup_priority() const { return setup_priority::DATA; } - void DHT::set_dht_model(DHTModel model) { this->model_ = model; this->is_auto_detect_ = model == DHT_MODEL_AUTO_DETECT; diff --git a/esphome/components/dht/dht.h b/esphome/components/dht/dht.h index 9047dd2c96..4671ee6f27 100644 --- a/esphome/components/dht/dht.h +++ b/esphome/components/dht/dht.h @@ -51,8 +51,6 @@ class DHT : public PollingComponent { void dump_config() override; /// Update sensor values and push them to the frontend. void update() override; - /// HARDWARE_LATE setup priority. - float get_setup_priority() const override; protected: bool read_sensor_(float *temperature, float *humidity, bool report_errors); diff --git a/esphome/components/dht12/dht12.cpp b/esphome/components/dht12/dht12.cpp index 445d150be0..1d884daad6 100644 --- a/esphome/components/dht12/dht12.cpp +++ b/esphome/components/dht12/dht12.cpp @@ -49,7 +49,7 @@ void DHT12Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float DHT12Component::get_setup_priority() const { return setup_priority::DATA; } + bool DHT12Component::read_data_(uint8_t *data) { if (!this->read_bytes(0, data, 5)) { ESP_LOGW(TAG, "Updating DHT12 failed!"); diff --git a/esphome/components/dht12/dht12.h b/esphome/components/dht12/dht12.h index 2a706039ba..ab19d7c723 100644 --- a/esphome/components/dht12/dht12.h +++ b/esphome/components/dht12/dht12.h @@ -11,7 +11,6 @@ class DHT12Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/display/__init__.py b/esphome/components/display/__init__.py index ccbeedcd2f..695e7cde47 100644 --- a/esphome/components/display/__init__.py +++ b/esphome/components/display/__init__.py @@ -15,7 +15,7 @@ from esphome.const import ( CONF_UPDATE_INTERVAL, SCHEDULER_DONT_RUN, ) -from esphome.core import CoroPriority, coroutine_with_priority +from esphome.core import CORE, CoroPriority, coroutine_with_priority IS_PLATFORM_COMPONENT = True @@ -222,3 +222,8 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg, async def to_code(config): cg.add_global(display_ns.using) cg.add_define("USE_DISPLAY") + if CORE.is_esp32: + # Re-enable ESP-IDF's LCD driver (excluded by default to save compile time) + from esphome.components.esp32 import include_builtin_idf_component + + include_builtin_idf_component("esp_lcd") diff --git a/esphome/components/dlms_meter/__init__.py b/esphome/components/dlms_meter/__init__.py new file mode 100644 index 0000000000..c22ab7b552 --- /dev/null +++ b/esphome/components/dlms_meter/__init__.py @@ -0,0 +1,57 @@ +import esphome.codegen as cg +from esphome.components import uart +import esphome.config_validation as cv +from esphome.const import CONF_ID, PLATFORM_ESP32, PLATFORM_ESP8266 + +CODEOWNERS = ["@SimonFischer04"] +DEPENDENCIES = ["uart"] + +CONF_DLMS_METER_ID = "dlms_meter_id" +CONF_DECRYPTION_KEY = "decryption_key" +CONF_PROVIDER = "provider" + +PROVIDERS = {"generic": 0, "netznoe": 1} + +dlms_meter_component_ns = cg.esphome_ns.namespace("dlms_meter") +DlmsMeterComponent = dlms_meter_component_ns.class_( + "DlmsMeterComponent", cg.Component, uart.UARTDevice +) + + +def validate_key(value): + value = cv.string_strict(value) + if len(value) != 32: + raise cv.Invalid("Decryption key must be 32 hex characters (16 bytes)") + try: + return [int(value[i : i + 2], 16) for i in range(0, 32, 2)] + except ValueError as exc: + raise cv.Invalid("Decryption key must be hex values from 00 to FF") from exc + + +CONFIG_SCHEMA = cv.All( + cv.Schema( + { + cv.GenerateID(): cv.declare_id(DlmsMeterComponent), + cv.Required(CONF_DECRYPTION_KEY): validate_key, + cv.Optional(CONF_PROVIDER, default="generic"): cv.enum( + PROVIDERS, lower=True + ), + } + ) + .extend(uart.UART_DEVICE_SCHEMA) + .extend(cv.COMPONENT_SCHEMA), + cv.only_on([PLATFORM_ESP8266, PLATFORM_ESP32]), +) + +FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema( + "dlms_meter", baud_rate=2400, require_rx=True +) + + +async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + await uart.register_uart_device(var, config) + key = ", ".join(str(b) for b in config[CONF_DECRYPTION_KEY]) + cg.add(var.set_decryption_key(cg.RawExpression(f"{{{key}}}"))) + cg.add(var.set_provider(PROVIDERS[config[CONF_PROVIDER]])) diff --git a/esphome/components/dlms_meter/dlms.h b/esphome/components/dlms_meter/dlms.h new file mode 100644 index 0000000000..a3d8f62ce6 --- /dev/null +++ b/esphome/components/dlms_meter/dlms.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +/* ++-------------------------------+ +| Ciphering Service | ++-------------------------------+ +| System Title Length | ++-------------------------------+ +| | +| | +| | +| System | +| Title | +| | +| | +| | ++-------------------------------+ +| Length | (1 or 3 Bytes) ++-------------------------------+ +| Security Control Byte | ++-------------------------------+ +| | +| Frame | +| Counter | +| | ++-------------------------------+ +| | +~ ~ + Encrypted Payload +~ ~ +| | ++-------------------------------+ + +Ciphering Service: 0xDB (General-Glo-Ciphering) +System Title Length: 0x08 +System Title: Unique ID of meter +Length: 1 Byte=Length <= 127, 3 Bytes=Length > 127 (0x82 & 2 Bytes length) +Security Control Byte: +- Bit 3…0: Security_Suite_Id +- Bit 4: "A" subfield: indicates that authentication is applied +- Bit 5: "E" subfield: indicates that encryption is applied +- Bit 6: Key_Set subfield: 0 = Unicast, 1 = Broadcast +- Bit 7: Indicates the use of compression. + */ + +static constexpr uint8_t DLMS_HEADER_LENGTH = 16; +static constexpr uint8_t DLMS_HEADER_EXT_OFFSET = 2; // Extra offset for extended length header +static constexpr uint8_t DLMS_CIPHER_OFFSET = 0; +static constexpr uint8_t DLMS_SYST_OFFSET = 1; +static constexpr uint8_t DLMS_LENGTH_OFFSET = 10; +static constexpr uint8_t TWO_BYTE_LENGTH = 0x82; +static constexpr uint8_t DLMS_LENGTH_CORRECTION = 5; // Header bytes included in length field +static constexpr uint8_t DLMS_SECBYTE_OFFSET = 11; +static constexpr uint8_t DLMS_FRAMECOUNTER_OFFSET = 12; +static constexpr uint8_t DLMS_FRAMECOUNTER_LENGTH = 4; +static constexpr uint8_t DLMS_PAYLOAD_OFFSET = 16; +static constexpr uint8_t GLO_CIPHERING = 0xDB; +static constexpr uint8_t DATA_NOTIFICATION = 0x0F; +static constexpr uint8_t TIMESTAMP_DATETIME = 0x0C; +static constexpr uint16_t MAX_MESSAGE_LENGTH = 512; // Maximum size of message (when having 2 bytes length in header). + +// Provider specific quirks +static constexpr uint8_t NETZ_NOE_MAGIC_BYTE = 0x81; // Magic length byte used by Netz NOE +static constexpr uint8_t NETZ_NOE_EXPECTED_MESSAGE_LENGTH = 0xF8; +static constexpr uint8_t NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE = 0x20; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp new file mode 100644 index 0000000000..6aa465143e --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -0,0 +1,468 @@ +#include "dlms_meter.h" + +#include + +#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) +#include +#elif defined(USE_ESP32) +#include "mbedtls/esp_config.h" +#include "mbedtls/gcm.h" +#endif + +namespace esphome::dlms_meter { + +static constexpr const char *TAG = "dlms_meter"; + +void DlmsMeterComponent::dump_config() { + const char *provider_name = this->provider_ == PROVIDER_NETZNOE ? "Netz NOE" : "Generic"; + ESP_LOGCONFIG(TAG, + "DLMS Meter:\n" + " Provider: %s\n" + " Read Timeout: %u ms", + provider_name, this->read_timeout_); +#define DLMS_METER_LOG_SENSOR(s) LOG_SENSOR(" ", #s, this->s##_sensor_); + DLMS_METER_SENSOR_LIST(DLMS_METER_LOG_SENSOR, ) +#define DLMS_METER_LOG_TEXT_SENSOR(s) LOG_TEXT_SENSOR(" ", #s, this->s##_text_sensor_); + DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_LOG_TEXT_SENSOR, ) +} + +void DlmsMeterComponent::loop() { + // Read while data is available, netznoe uses two frames so allow 2x max frame length + while (this->available()) { + if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) { + ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); + break; + } + uint8_t c; + this->read_byte(&c); + this->receive_buffer_.push_back(c); + this->last_read_ = millis(); + } + + if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) { + this->mbus_payload_.clear(); + if (!this->parse_mbus_(this->mbus_payload_)) + return; + + uint16_t message_length; + uint8_t systitle_length; + uint16_t header_offset; + if (!this->parse_dlms_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + + if (message_length < DECODER_START_OFFSET || message_length > MAX_MESSAGE_LENGTH) { + ESP_LOGE(TAG, "DLMS: Message length invalid: %u", message_length); + this->receive_buffer_.clear(); + return; + } + + // Decrypt in place and then decode the OBIS codes + if (!this->decrypt_(this->mbus_payload_, message_length, systitle_length, header_offset)) + return; + this->decode_obis_(&this->mbus_payload_[header_offset + DLMS_PAYLOAD_OFFSET], message_length); + } +} + +bool DlmsMeterComponent::parse_mbus_(std::vector &mbus_payload) { + ESP_LOGV(TAG, "Parsing M-Bus frames"); + uint16_t frame_offset = 0; // Offset is used if the M-Bus message is split into multiple frames + + while (frame_offset < this->receive_buffer_.size()) { + // Ensure enough bytes remain for the minimal intro header before accessing indices + if (this->receive_buffer_.size() - frame_offset < MBUS_HEADER_INTRO_LENGTH) { + ESP_LOGE(TAG, "MBUS: Not enough data for frame header (need %d, have %d)", MBUS_HEADER_INTRO_LENGTH, + (this->receive_buffer_.size() - frame_offset)); + this->receive_buffer_.clear(); + return false; + } + + // Check start bytes + if (this->receive_buffer_[frame_offset + MBUS_START1_OFFSET] != START_BYTE_LONG_FRAME || + this->receive_buffer_[frame_offset + MBUS_START2_OFFSET] != START_BYTE_LONG_FRAME) { + ESP_LOGE(TAG, "MBUS: Start bytes do not match"); + this->receive_buffer_.clear(); + return false; + } + + // Both length bytes must be identical + if (this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET] != + this->receive_buffer_[frame_offset + MBUS_LENGTH2_OFFSET]) { + ESP_LOGE(TAG, "MBUS: Length bytes do not match"); + this->receive_buffer_.clear(); + return false; + } + + uint8_t frame_length = this->receive_buffer_[frame_offset + MBUS_LENGTH1_OFFSET]; // Get length of this frame + + // Check if received data is enough for the given frame length + if (this->receive_buffer_.size() - frame_offset < + frame_length + 3) { // length field inside packet does not account for second start- + checksum- + stop- byte + ESP_LOGE(TAG, "MBUS: Frame too big for received data"); + this->receive_buffer_.clear(); + return false; + } + + // Ensure we have full frame (header + payload + checksum + stop byte) before accessing stop byte + size_t required_total = + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH; // payload + header + 2 footer bytes + if (this->receive_buffer_.size() - frame_offset < required_total) { + ESP_LOGE(TAG, "MBUS: Incomplete frame (need %d, have %d)", (unsigned int) required_total, + this->receive_buffer_.size() - frame_offset); + this->receive_buffer_.clear(); + return false; + } + + if (this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH + MBUS_FOOTER_LENGTH - 1] != + STOP_BYTE) { + ESP_LOGE(TAG, "MBUS: Invalid stop byte"); + this->receive_buffer_.clear(); + return false; + } + + // Verify checksum: sum of all bytes starting at MBUS_HEADER_INTRO_LENGTH, take last byte + uint8_t checksum = 0; // use uint8_t so only the 8 least significant bits are stored + for (uint16_t i = 0; i < frame_length; i++) { + checksum += this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + i]; + } + if (checksum != this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]) { + ESP_LOGE(TAG, "MBUS: Invalid checksum: %x != %x", checksum, + this->receive_buffer_[frame_offset + frame_length + MBUS_HEADER_INTRO_LENGTH]); + this->receive_buffer_.clear(); + return false; + } + + mbus_payload.insert(mbus_payload.end(), &this->receive_buffer_[frame_offset + MBUS_FULL_HEADER_LENGTH], + &this->receive_buffer_[frame_offset + MBUS_HEADER_INTRO_LENGTH + frame_length]); + + frame_offset += MBUS_HEADER_INTRO_LENGTH + frame_length + MBUS_FOOTER_LENGTH; + } + return true; +} + +bool DlmsMeterComponent::parse_dlms_(const std::vector &mbus_payload, uint16_t &message_length, + uint8_t &systitle_length, uint16_t &header_offset) { + ESP_LOGV(TAG, "Parsing DLMS header"); + if (mbus_payload.size() < DLMS_HEADER_LENGTH + DLMS_HEADER_EXT_OFFSET) { + ESP_LOGE(TAG, "DLMS: Payload too short"); + this->receive_buffer_.clear(); + return false; + } + + if (mbus_payload[DLMS_CIPHER_OFFSET] != GLO_CIPHERING) { // Only general-glo-ciphering is supported (0xDB) + ESP_LOGE(TAG, "DLMS: Unsupported cipher"); + this->receive_buffer_.clear(); + return false; + } + + systitle_length = mbus_payload[DLMS_SYST_OFFSET]; + + if (systitle_length != 0x08) { // Only system titles with length of 8 are supported + ESP_LOGE(TAG, "DLMS: Unsupported system title length"); + this->receive_buffer_.clear(); + return false; + } + + message_length = mbus_payload[DLMS_LENGTH_OFFSET]; + header_offset = 0; + + if (this->provider_ == PROVIDER_NETZNOE) { + // for some reason EVN seems to set the standard "length" field to 0x81 and then the actual length is in the next + // byte. Check some bytes to see if received data still matches expectation + if (message_length == NETZ_NOE_MAGIC_BYTE && + mbus_payload[DLMS_LENGTH_OFFSET + 1] == NETZ_NOE_EXPECTED_MESSAGE_LENGTH && + mbus_payload[DLMS_LENGTH_OFFSET + 2] == NETZ_NOE_EXPECTED_SECURITY_CONTROL_BYTE) { + message_length = mbus_payload[DLMS_LENGTH_OFFSET + 1]; + header_offset = 1; + } else { + ESP_LOGE(TAG, "Wrong Length - Security Control Byte sequence detected for provider EVN"); + } + } else { + if (message_length == TWO_BYTE_LENGTH) { + message_length = encode_uint16(mbus_payload[DLMS_LENGTH_OFFSET + 1], mbus_payload[DLMS_LENGTH_OFFSET + 2]); + header_offset = DLMS_HEADER_EXT_OFFSET; + } + } + if (message_length < DLMS_LENGTH_CORRECTION) { + ESP_LOGE(TAG, "DLMS: Message length too short: %u", message_length); + this->receive_buffer_.clear(); + return false; + } + message_length -= DLMS_LENGTH_CORRECTION; // Correct message length due to part of header being included in length + + if (mbus_payload.size() - DLMS_HEADER_LENGTH - header_offset != message_length) { + ESP_LOGV(TAG, "DLMS: Length mismatch - payload=%d, header=%d, offset=%d, message=%d", mbus_payload.size(), + DLMS_HEADER_LENGTH, header_offset, message_length); + ESP_LOGE(TAG, "DLMS: Message has invalid length"); + this->receive_buffer_.clear(); + return false; + } + + if (mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != 0x21 && + mbus_payload[header_offset + DLMS_SECBYTE_OFFSET] != + 0x20) { // Only certain security suite is supported (0x21 || 0x20) + ESP_LOGE(TAG, "DLMS: Unsupported security control byte"); + this->receive_buffer_.clear(); + return false; + } + + return true; +} + +bool DlmsMeterComponent::decrypt_(std::vector &mbus_payload, uint16_t message_length, uint8_t systitle_length, + uint16_t header_offset) { + ESP_LOGV(TAG, "Decrypting payload"); + uint8_t iv[12]; // Reserve space for the IV, always 12 bytes + // Copy system title to IV (System title is before length; no header offset needed!) + // Add 1 to the offset in order to skip the system title length byte + memcpy(&iv[0], &mbus_payload[DLMS_SYST_OFFSET + 1], systitle_length); + memcpy(&iv[8], &mbus_payload[header_offset + DLMS_FRAMECOUNTER_OFFSET], + DLMS_FRAMECOUNTER_LENGTH); // Copy frame counter to IV + + uint8_t *payload_ptr = &mbus_payload[header_offset + DLMS_PAYLOAD_OFFSET]; + +#if defined(USE_ESP8266_FRAMEWORK_ARDUINO) + br_gcm_context gcm_ctx; + br_aes_ct_ctr_keys bc; + br_aes_ct_ctr_init(&bc, this->decryption_key_.data(), this->decryption_key_.size()); + br_gcm_init(&gcm_ctx, &bc.vtable, br_ghash_ctmul32); + br_gcm_reset(&gcm_ctx, iv, sizeof(iv)); + br_gcm_flip(&gcm_ctx); + br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length); +#elif defined(USE_ESP32) + size_t outlen = 0; + mbedtls_gcm_context gcm_ctx; + mbedtls_gcm_init(&gcm_ctx); + mbedtls_gcm_setkey(&gcm_ctx, MBEDTLS_CIPHER_ID_AES, this->decryption_key_.data(), this->decryption_key_.size() * 8); + mbedtls_gcm_starts(&gcm_ctx, MBEDTLS_GCM_DECRYPT, iv, sizeof(iv)); + auto ret = mbedtls_gcm_update(&gcm_ctx, payload_ptr, message_length, payload_ptr, message_length, &outlen); + mbedtls_gcm_free(&gcm_ctx); + if (ret != 0) { + ESP_LOGE(TAG, "Decryption failed with error: %d", ret); + this->receive_buffer_.clear(); + return false; + } +#else +#error "Invalid Platform" +#endif + + if (payload_ptr[0] != DATA_NOTIFICATION || payload_ptr[5] != TIMESTAMP_DATETIME) { + ESP_LOGE(TAG, "OBIS: Packet was decrypted but data is invalid"); + this->receive_buffer_.clear(); + return false; + } + ESP_LOGV(TAG, "Decrypted payload: %d bytes", message_length); + return true; +} + +void DlmsMeterComponent::decode_obis_(uint8_t *plaintext, uint16_t message_length) { + ESP_LOGV(TAG, "Decoding payload"); + MeterData data{}; + uint16_t current_position = DECODER_START_OFFSET; + bool power_factor_found = false; + + while (current_position + OBIS_CODE_OFFSET <= message_length) { + if (plaintext[current_position + OBIS_TYPE_OFFSET] != DataType::OCTET_STRING) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS header type: %x", plaintext[current_position + OBIS_TYPE_OFFSET]); + this->receive_buffer_.clear(); + return; + } + + uint8_t obis_code_length = plaintext[current_position + OBIS_LENGTH_OFFSET]; + if (obis_code_length != OBIS_CODE_LENGTH_STANDARD && obis_code_length != OBIS_CODE_LENGTH_EXTENDED) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS header length: %x", obis_code_length); + this->receive_buffer_.clear(); + return; + } + if (current_position + OBIS_CODE_OFFSET + obis_code_length > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for OBIS code"); + this->receive_buffer_.clear(); + return; + } + + uint8_t *obis_code = &plaintext[current_position + OBIS_CODE_OFFSET]; + uint8_t obis_medium = obis_code[OBIS_A]; + uint16_t obis_cd = encode_uint16(obis_code[OBIS_C], obis_code[OBIS_D]); + + bool timestamp_found = false; + bool meter_number_found = false; + if (this->provider_ == PROVIDER_NETZNOE) { + // Do not advance Position when reading the Timestamp at DECODER_START_OFFSET + if ((obis_code_length == OBIS_CODE_LENGTH_EXTENDED) && (current_position == DECODER_START_OFFSET)) { + timestamp_found = true; + } else if (power_factor_found) { + meter_number_found = true; + power_factor_found = false; + } else { + current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code and position + } + } else { + current_position += obis_code_length + OBIS_CODE_OFFSET; // Advance past code, position and type + } + if (!timestamp_found && !meter_number_found && obis_medium != Medium::ELECTRICITY && + obis_medium != Medium::ABSTRACT) { + ESP_LOGE(TAG, "OBIS: Unsupported OBIS medium: %x", obis_medium); + this->receive_buffer_.clear(); + return; + } + + if (current_position >= message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for data type"); + this->receive_buffer_.clear(); + return; + } + + float value = 0.0f; + uint8_t value_size = 0; + uint8_t data_type = plaintext[current_position]; + current_position++; + + switch (data_type) { + case DataType::DOUBLE_LONG_UNSIGNED: { + value_size = 4; + if (current_position + value_size > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for DOUBLE_LONG_UNSIGNED"); + this->receive_buffer_.clear(); + return; + } + value = encode_uint32(plaintext[current_position + 0], plaintext[current_position + 1], + plaintext[current_position + 2], plaintext[current_position + 3]); + current_position += value_size; + break; + } + case DataType::LONG_UNSIGNED: { + value_size = 2; + if (current_position + value_size > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for LONG_UNSIGNED"); + this->receive_buffer_.clear(); + return; + } + value = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]); + current_position += value_size; + break; + } + case DataType::OCTET_STRING: { + uint8_t data_length = plaintext[current_position]; + current_position++; // Advance past string length + if (current_position + data_length > message_length) { + ESP_LOGE(TAG, "OBIS: Buffer too short for OCTET_STRING"); + this->receive_buffer_.clear(); + return; + } + // Handle timestamp (normal OBIS code or NETZNOE special case) + if (obis_cd == OBIS_TIMESTAMP || timestamp_found) { + if (data_length < 8) { + ESP_LOGE(TAG, "OBIS: Timestamp data too short: %u", data_length); + this->receive_buffer_.clear(); + return; + } + uint16_t year = encode_uint16(plaintext[current_position + 0], plaintext[current_position + 1]); + uint8_t month = plaintext[current_position + 2]; + uint8_t day = plaintext[current_position + 3]; + uint8_t hour = plaintext[current_position + 5]; + uint8_t minute = plaintext[current_position + 6]; + uint8_t second = plaintext[current_position + 7]; + if (year > 9999 || month > 12 || day > 31 || hour > 23 || minute > 59 || second > 59) { + ESP_LOGE(TAG, "Invalid timestamp values: %04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, minute, + second); + this->receive_buffer_.clear(); + return; + } + snprintf(data.timestamp, sizeof(data.timestamp), "%04u-%02u-%02uT%02u:%02u:%02uZ", year, month, day, hour, + minute, second); + } else if (meter_number_found) { + snprintf(data.meternumber, sizeof(data.meternumber), "%.*s", data_length, &plaintext[current_position]); + } + current_position += data_length; + break; + } + default: + ESP_LOGE(TAG, "OBIS: Unsupported OBIS data type: %x", data_type); + this->receive_buffer_.clear(); + return; + } + + // Skip break after data + if (this->provider_ == PROVIDER_NETZNOE) { + // Don't skip the break on the first timestamp, as there's none + if (!timestamp_found) { + current_position += 2; + } + } else { + current_position += 2; + } + + // Check for additional data (scaler-unit structure) + if (current_position < message_length && plaintext[current_position] == DataType::INTEGER) { + // Apply scaler: real_value = raw_value × 10^scaler + if (current_position + 1 < message_length) { + int8_t scaler = static_cast(plaintext[current_position + 1]); + if (scaler != 0) { + value *= powf(10.0f, scaler); + } + } + + // on EVN Meters there is no additional break + if (this->provider_ == PROVIDER_NETZNOE) { + current_position += 4; + } else { + current_position += 6; + } + } + + // Handle numeric values (LONG_UNSIGNED and DOUBLE_LONG_UNSIGNED) + if (value_size > 0) { + switch (obis_cd) { + case OBIS_VOLTAGE_L1: + data.voltage_l1 = value; + break; + case OBIS_VOLTAGE_L2: + data.voltage_l2 = value; + break; + case OBIS_VOLTAGE_L3: + data.voltage_l3 = value; + break; + case OBIS_CURRENT_L1: + data.current_l1 = value; + break; + case OBIS_CURRENT_L2: + data.current_l2 = value; + break; + case OBIS_CURRENT_L3: + data.current_l3 = value; + break; + case OBIS_ACTIVE_POWER_PLUS: + data.active_power_plus = value; + break; + case OBIS_ACTIVE_POWER_MINUS: + data.active_power_minus = value; + break; + case OBIS_ACTIVE_ENERGY_PLUS: + data.active_energy_plus = value; + break; + case OBIS_ACTIVE_ENERGY_MINUS: + data.active_energy_minus = value; + break; + case OBIS_REACTIVE_ENERGY_PLUS: + data.reactive_energy_plus = value; + break; + case OBIS_REACTIVE_ENERGY_MINUS: + data.reactive_energy_minus = value; + break; + case OBIS_POWER_FACTOR: + data.power_factor = value; + power_factor_found = true; + break; + default: + ESP_LOGW(TAG, "Unsupported OBIS code 0x%04X", obis_cd); + } + } + } + + this->receive_buffer_.clear(); + + ESP_LOGI(TAG, "Received valid data"); + this->publish_sensors(data); + this->status_clear_warning(); +} + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/dlms_meter.h b/esphome/components/dlms_meter/dlms_meter.h new file mode 100644 index 0000000000..c50e6f6b4d --- /dev/null +++ b/esphome/components/dlms_meter/dlms_meter.h @@ -0,0 +1,96 @@ +#pragma once + +#include "esphome/core/component.h" +#include "esphome/core/defines.h" +#include "esphome/core/log.h" +#ifdef USE_SENSOR +#include "esphome/components/sensor/sensor.h" +#endif +#ifdef USE_TEXT_SENSOR +#include "esphome/components/text_sensor/text_sensor.h" +#endif +#include "esphome/components/uart/uart.h" + +#include "mbus.h" +#include "dlms.h" +#include "obis.h" + +#include +#include + +namespace esphome::dlms_meter { + +#ifndef DLMS_METER_SENSOR_LIST +#define DLMS_METER_SENSOR_LIST(F, SEP) +#endif + +#ifndef DLMS_METER_TEXT_SENSOR_LIST +#define DLMS_METER_TEXT_SENSOR_LIST(F, SEP) +#endif + +struct MeterData { + float voltage_l1 = 0.0f; // Voltage L1 + float voltage_l2 = 0.0f; // Voltage L2 + float voltage_l3 = 0.0f; // Voltage L3 + float current_l1 = 0.0f; // Current L1 + float current_l2 = 0.0f; // Current L2 + float current_l3 = 0.0f; // Current L3 + float active_power_plus = 0.0f; // Active power taken from grid + float active_power_minus = 0.0f; // Active power put into grid + float active_energy_plus = 0.0f; // Active energy taken from grid + float active_energy_minus = 0.0f; // Active energy put into grid + float reactive_energy_plus = 0.0f; // Reactive energy taken from grid + float reactive_energy_minus = 0.0f; // Reactive energy put into grid + char timestamp[27]{}; // Text sensor for the timestamp value + + // Netz NOE + float power_factor = 0.0f; // Power Factor + char meternumber[13]{}; // Text sensor for the meterNumber value +}; + +// Provider constants +enum Providers : uint32_t { PROVIDER_GENERIC = 0x00, PROVIDER_NETZNOE = 0x01 }; + +class DlmsMeterComponent : public Component, public uart::UARTDevice { + public: + DlmsMeterComponent() = default; + + void dump_config() override; + void loop() override; + + void set_decryption_key(const std::array &key) { this->decryption_key_ = key; } + void set_provider(uint32_t provider) { this->provider_ = provider; } + + void publish_sensors(MeterData &data) { +#define DLMS_METER_PUBLISH_SENSOR(s) \ + if (this->s##_sensor_ != nullptr) \ + s##_sensor_->publish_state(data.s); + DLMS_METER_SENSOR_LIST(DLMS_METER_PUBLISH_SENSOR, ) + +#define DLMS_METER_PUBLISH_TEXT_SENSOR(s) \ + if (this->s##_text_sensor_ != nullptr) \ + s##_text_sensor_->publish_state(data.s); + DLMS_METER_TEXT_SENSOR_LIST(DLMS_METER_PUBLISH_TEXT_SENSOR, ) + } + + DLMS_METER_SENSOR_LIST(SUB_SENSOR, ) + DLMS_METER_TEXT_SENSOR_LIST(SUB_TEXT_SENSOR, ) + + protected: + bool parse_mbus_(std::vector &mbus_payload); + bool parse_dlms_(const std::vector &mbus_payload, uint16_t &message_length, uint8_t &systitle_length, + uint16_t &header_offset); + bool decrypt_(std::vector &mbus_payload, uint16_t message_length, uint8_t systitle_length, + uint16_t header_offset); + void decode_obis_(uint8_t *plaintext, uint16_t message_length); + + std::vector receive_buffer_; // Stores the packet currently being received + std::vector mbus_payload_; // Parsed M-Bus payload, reused to avoid heap churn + uint32_t last_read_ = 0; // Timestamp when data was last read + uint32_t read_timeout_ = 1000; // Time to wait after last byte before considering data complete + + uint32_t provider_ = PROVIDER_GENERIC; // Provider of the meter / your grid operator + std::array decryption_key_; +}; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/mbus.h b/esphome/components/dlms_meter/mbus.h new file mode 100644 index 0000000000..293d43a55b --- /dev/null +++ b/esphome/components/dlms_meter/mbus.h @@ -0,0 +1,69 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +/* ++----------------------------------------------------+ - +| Start Character [0x68] | \ ++----------------------------------------------------+ | +| Data Length (L) | | ++----------------------------------------------------+ | +| Data Length Repeat (L) | | ++----------------------------------------------------+ > M-Bus Data link layer +| Start Character Repeat [0x68] | | ++----------------------------------------------------+ | +| Control/Function Field (C) | | ++----------------------------------------------------+ | +| Address Field (A) | / ++----------------------------------------------------+ - +| Control Information Field (CI) | \ ++----------------------------------------------------+ | +| Source Transport Service Access Point (STSAP) | > DLMS/COSEM M-Bus transport layer ++----------------------------------------------------+ | +| Destination Transport Service Access Point (DTSAP) | / ++----------------------------------------------------+ - +| | \ +~ ~ | + Data > DLMS/COSEM Application Layer +~ ~ | +| | / ++----------------------------------------------------+ - +| Checksum | \ ++----------------------------------------------------+ > M-Bus Data link layer +| Stop Character [0x16] | / ++----------------------------------------------------+ - + +Data_Length = L - C - A - CI +Each line (except Data) is one Byte + +Possible Values found in publicly available docs: +- C: 0x53/0x73 (SND_UD) +- A: FF (Broadcast) +- CI: 0x00-0x1F/0x60/0x61/0x7C/0x7D +- STSAP: 0x01 (Management Logical Device ID 1 of the meter) +- DTSAP: 0x67 (Consumer Information Push Client ID 103) + */ + +// MBUS start bytes for different telegram formats: +// - Single Character: 0xE5 (length=1) +// - Short Frame: 0x10 (length=5) +// - Control Frame: 0x68 (length=9) +// - Long Frame: 0x68 (length=9+data_length) +// This component currently only uses Long Frame. +static constexpr uint8_t START_BYTE_SINGLE_CHARACTER = 0xE5; +static constexpr uint8_t START_BYTE_SHORT_FRAME = 0x10; +static constexpr uint8_t START_BYTE_CONTROL_FRAME = 0x68; +static constexpr uint8_t START_BYTE_LONG_FRAME = 0x68; +static constexpr uint8_t MBUS_HEADER_INTRO_LENGTH = 4; // Header length for the intro (0x68, length, length, 0x68) +static constexpr uint8_t MBUS_FULL_HEADER_LENGTH = 9; // Total header length +static constexpr uint8_t MBUS_FOOTER_LENGTH = 2; // Footer after frame +static constexpr uint8_t MBUS_MAX_FRAME_LENGTH = 250; // Maximum size of frame +static constexpr uint8_t MBUS_START1_OFFSET = 0; // Offset of first start byte +static constexpr uint8_t MBUS_LENGTH1_OFFSET = 1; // Offset of first length byte +static constexpr uint8_t MBUS_LENGTH2_OFFSET = 2; // Offset of (duplicated) second length byte +static constexpr uint8_t MBUS_START2_OFFSET = 3; // Offset of (duplicated) second start byte +static constexpr uint8_t STOP_BYTE = 0x16; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/obis.h b/esphome/components/dlms_meter/obis.h new file mode 100644 index 0000000000..1bb960e61e --- /dev/null +++ b/esphome/components/dlms_meter/obis.h @@ -0,0 +1,94 @@ +#pragma once + +#include + +namespace esphome::dlms_meter { + +// Data types as per specification +enum DataType { + NULL_DATA = 0x00, + BOOLEAN = 0x03, + BIT_STRING = 0x04, + DOUBLE_LONG = 0x05, + DOUBLE_LONG_UNSIGNED = 0x06, + OCTET_STRING = 0x09, + VISIBLE_STRING = 0x0A, + UTF8_STRING = 0x0C, + BINARY_CODED_DECIMAL = 0x0D, + INTEGER = 0x0F, + LONG = 0x10, + UNSIGNED = 0x11, + LONG_UNSIGNED = 0x12, + LONG64 = 0x14, + LONG64_UNSIGNED = 0x15, + ENUM = 0x16, + FLOAT32 = 0x17, + FLOAT64 = 0x18, + DATE_TIME = 0x19, + DATE = 0x1A, + TIME = 0x1B, + + ARRAY = 0x01, + STRUCTURE = 0x02, + COMPACT_ARRAY = 0x13 +}; + +enum Medium { + ABSTRACT = 0x00, + ELECTRICITY = 0x01, + HEAT_COST_ALLOCATOR = 0x04, + COOLING = 0x05, + HEAT = 0x06, + GAS = 0x07, + COLD_WATER = 0x08, + HOT_WATER = 0x09, + OIL = 0x10, + COMPRESSED_AIR = 0x11, + NITROGEN = 0x12 +}; + +// Data structure +static constexpr uint8_t DECODER_START_OFFSET = 20; // Skip header, timestamp and break block +static constexpr uint8_t OBIS_TYPE_OFFSET = 0; +static constexpr uint8_t OBIS_LENGTH_OFFSET = 1; +static constexpr uint8_t OBIS_CODE_OFFSET = 2; +static constexpr uint8_t OBIS_CODE_LENGTH_STANDARD = 0x06; // 6-byte OBIS code (A.B.C.D.E.F) +static constexpr uint8_t OBIS_CODE_LENGTH_EXTENDED = 0x0C; // 12-byte extended OBIS code +static constexpr uint8_t OBIS_A = 0; +static constexpr uint8_t OBIS_B = 1; +static constexpr uint8_t OBIS_C = 2; +static constexpr uint8_t OBIS_D = 3; +static constexpr uint8_t OBIS_E = 4; +static constexpr uint8_t OBIS_F = 5; + +// Metadata +static constexpr uint16_t OBIS_TIMESTAMP = 0x0100; +static constexpr uint16_t OBIS_SERIAL_NUMBER = 0x6001; +static constexpr uint16_t OBIS_DEVICE_NAME = 0x2A00; + +// Voltage +static constexpr uint16_t OBIS_VOLTAGE_L1 = 0x2007; +static constexpr uint16_t OBIS_VOLTAGE_L2 = 0x3407; +static constexpr uint16_t OBIS_VOLTAGE_L3 = 0x4807; + +// Current +static constexpr uint16_t OBIS_CURRENT_L1 = 0x1F07; +static constexpr uint16_t OBIS_CURRENT_L2 = 0x3307; +static constexpr uint16_t OBIS_CURRENT_L3 = 0x4707; + +// Power +static constexpr uint16_t OBIS_ACTIVE_POWER_PLUS = 0x0107; +static constexpr uint16_t OBIS_ACTIVE_POWER_MINUS = 0x0207; + +// Active energy +static constexpr uint16_t OBIS_ACTIVE_ENERGY_PLUS = 0x0108; +static constexpr uint16_t OBIS_ACTIVE_ENERGY_MINUS = 0x0208; + +// Reactive energy +static constexpr uint16_t OBIS_REACTIVE_ENERGY_PLUS = 0x0308; +static constexpr uint16_t OBIS_REACTIVE_ENERGY_MINUS = 0x0408; + +// Netz NOE specific +static constexpr uint16_t OBIS_POWER_FACTOR = 0x0D07; + +} // namespace esphome::dlms_meter diff --git a/esphome/components/dlms_meter/sensor/__init__.py b/esphome/components/dlms_meter/sensor/__init__.py new file mode 100644 index 0000000000..27fd44f008 --- /dev/null +++ b/esphome/components/dlms_meter/sensor/__init__.py @@ -0,0 +1,124 @@ +import esphome.codegen as cg +from esphome.components import sensor +import esphome.config_validation as cv +from esphome.const import ( + CONF_ID, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_POWER_FACTOR, + DEVICE_CLASS_VOLTAGE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_AMPERE, + UNIT_VOLT, + UNIT_WATT, + UNIT_WATT_HOURS, +) + +from .. import CONF_DLMS_METER_ID, DlmsMeterComponent + +AUTO_LOAD = ["dlms_meter"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), + cv.Optional("voltage_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("voltage_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_VOLT, + accuracy_decimals=1, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l1"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l2"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("current_l3"): sensor.sensor_schema( + unit_of_measurement=UNIT_AMPERE, + accuracy_decimals=2, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_power_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_power_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional("active_energy_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("active_energy_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("reactive_energy_plus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional("reactive_energy_minus"): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + # Netz NOE + cv.Optional("power_factor"): sensor.sensor_schema( + accuracy_decimals=3, + device_class=DEVICE_CLASS_POWER_FACTOR, + state_class=STATE_CLASS_MEASUREMENT, + ), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) + + sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf[CONF_ID] + if id and id.type == sensor.Sensor: + sens = await sensor.new_sensor(conf) + cg.add(getattr(hub, f"set_{key}_sensor")(sens)) + sensors.append(f"F({key})") + + if sensors: + cg.add_define( + "DLMS_METER_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors)) + ) diff --git a/esphome/components/dlms_meter/text_sensor/__init__.py b/esphome/components/dlms_meter/text_sensor/__init__.py new file mode 100644 index 0000000000..4d2373f4f9 --- /dev/null +++ b/esphome/components/dlms_meter/text_sensor/__init__.py @@ -0,0 +1,37 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +import esphome.config_validation as cv +from esphome.const import CONF_ID + +from .. import CONF_DLMS_METER_ID, DlmsMeterComponent + +AUTO_LOAD = ["dlms_meter"] + +CONFIG_SCHEMA = cv.Schema( + { + cv.GenerateID(CONF_DLMS_METER_ID): cv.use_id(DlmsMeterComponent), + cv.Optional("timestamp"): text_sensor.text_sensor_schema(), + # Netz NOE + cv.Optional("meternumber"): text_sensor.text_sensor_schema(), + } +).extend(cv.COMPONENT_SCHEMA) + + +async def to_code(config): + hub = await cg.get_variable(config[CONF_DLMS_METER_ID]) + + text_sensors = [] + for key, conf in config.items(): + if not isinstance(conf, dict): + continue + id = conf[CONF_ID] + if id and id.type == text_sensor.TextSensor: + sens = await text_sensor.new_text_sensor(conf) + cg.add(getattr(hub, f"set_{key}_text_sensor")(sens)) + text_sensors.append(f"F({key})") + + if text_sensors: + cg.add_define( + "DLMS_METER_TEXT_SENSOR_LIST(F, sep)", + cg.RawExpression(" sep ".join(text_sensors)), + ) diff --git a/esphome/components/dps310/dps310.cpp b/esphome/components/dps310/dps310.cpp index 6b6f9622fa..aa0a77cdd8 100644 --- a/esphome/components/dps310/dps310.cpp +++ b/esphome/components/dps310/dps310.cpp @@ -98,8 +98,6 @@ void DPS310Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } - void DPS310Component::update() { if (!this->update_in_progress_) { this->update_in_progress_ = true; diff --git a/esphome/components/dps310/dps310.h b/esphome/components/dps310/dps310.h index 50e7d93c8a..dce220d44b 100644 --- a/esphome/components/dps310/dps310.h +++ b/esphome/components/dps310/dps310.h @@ -40,7 +40,6 @@ class DPS310Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/ds1307/ds1307.cpp b/esphome/components/ds1307/ds1307.cpp index adbd7b5487..5c0e98290b 100644 --- a/esphome/components/ds1307/ds1307.cpp +++ b/esphome/components/ds1307/ds1307.cpp @@ -26,8 +26,6 @@ void DS1307Component::dump_config() { RealTimeClock::dump_config(); } -float DS1307Component::get_setup_priority() const { return setup_priority::DATA; } - void DS1307Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/ds1307/ds1307.h b/esphome/components/ds1307/ds1307.h index f7f06253b7..1712056006 100644 --- a/esphome/components/ds1307/ds1307.h +++ b/esphome/components/ds1307/ds1307.h @@ -12,7 +12,6 @@ class DS1307Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.cpp b/esphome/components/duty_cycle/duty_cycle_sensor.cpp index 40a728d025..f801769d27 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.cpp +++ b/esphome/components/duty_cycle/duty_cycle_sensor.cpp @@ -43,8 +43,6 @@ void DutyCycleSensor::update() { this->last_update_ = now; } -float DutyCycleSensor::get_setup_priority() const { return setup_priority::DATA; } - void IRAM_ATTR DutyCycleSensorStore::gpio_intr(DutyCycleSensorStore *arg) { const bool new_level = arg->pin.digital_read(); if (new_level == arg->last_level) diff --git a/esphome/components/duty_cycle/duty_cycle_sensor.h b/esphome/components/duty_cycle/duty_cycle_sensor.h index ffb1802e14..ffb8e3b622 100644 --- a/esphome/components/duty_cycle/duty_cycle_sensor.h +++ b/esphome/components/duty_cycle/duty_cycle_sensor.h @@ -22,7 +22,6 @@ class DutyCycleSensor : public sensor::Sensor, public PollingComponent { void set_pin(InternalGPIOPin *pin) { pin_ = pin; } void setup() override; - float get_setup_priority() const override; void dump_config() override; void update() override; diff --git a/esphome/components/ee895/ee895.cpp b/esphome/components/ee895/ee895.cpp index 602e31db14..22f28be9bc 100644 --- a/esphome/components/ee895/ee895.cpp +++ b/esphome/components/ee895/ee895.cpp @@ -55,8 +55,6 @@ void EE895Component::dump_config() { LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float EE895Component::get_setup_priority() const { return setup_priority::DATA; } - void EE895Component::update() { write_command_(TEMPERATURE_ADDRESS, 2); this->set_timeout(50, [this]() { diff --git a/esphome/components/ee895/ee895.h b/esphome/components/ee895/ee895.h index 83bd7c6e82..259b7c524b 100644 --- a/esphome/components/ee895/ee895.h +++ b/esphome/components/ee895/ee895.h @@ -14,7 +14,6 @@ class EE895Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.cpp b/esphome/components/emc2101/sensor/emc2101_sensor.cpp index 2a199f48e9..3014c7da07 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.cpp +++ b/esphome/components/emc2101/sensor/emc2101_sensor.cpp @@ -7,8 +7,6 @@ namespace emc2101 { static const char *const TAG = "EMC2101.sensor"; -float EMC2101Sensor::get_setup_priority() const { return setup_priority::DATA; } - void EMC2101Sensor::dump_config() { ESP_LOGCONFIG(TAG, "Emc2101 sensor:"); LOG_SENSOR(" ", "Internal temperature", this->internal_temperature_sensor_); diff --git a/esphome/components/emc2101/sensor/emc2101_sensor.h b/esphome/components/emc2101/sensor/emc2101_sensor.h index 3e8dcebc8e..3e033f58a7 100644 --- a/esphome/components/emc2101/sensor/emc2101_sensor.h +++ b/esphome/components/emc2101/sensor/emc2101_sensor.h @@ -15,8 +15,6 @@ class EMC2101Sensor : public PollingComponent { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; /** Used by ESPHome framework. */ void set_internal_temperature_sensor(sensor::Sensor *sensor) { this->internal_temperature_sensor_ = sensor; } diff --git a/esphome/components/endstop/endstop_cover.cpp b/esphome/components/endstop/endstop_cover.cpp index 2c281ea2e6..ea8a5ec186 100644 --- a/esphome/components/endstop/endstop_cover.cpp +++ b/esphome/components/endstop/endstop_cover.cpp @@ -111,7 +111,7 @@ void EndstopCover::dump_config() { LOG_BINARY_SENSOR(" ", "Open Endstop", this->open_endstop_); LOG_BINARY_SENSOR(" ", "Close Endstop", this->close_endstop_); } -float EndstopCover::get_setup_priority() const { return setup_priority::DATA; } + void EndstopCover::stop_prev_trigger_() { if (this->prev_command_trigger_ != nullptr) { this->prev_command_trigger_->stop_action(); @@ -141,15 +141,15 @@ void EndstopCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/endstop/endstop_cover.h b/esphome/components/endstop/endstop_cover.h index 6ae15de8c1..32ede12335 100644 --- a/esphome/components/endstop/endstop_cover.h +++ b/esphome/components/endstop/endstop_cover.h @@ -13,11 +13,10 @@ class EndstopCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } void set_open_endstop(binary_sensor::BinarySensor *open_endstop) { this->open_endstop_ = open_endstop; } void set_close_endstop(binary_sensor::BinarySensor *close_endstop) { this->close_endstop_ = close_endstop; } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } @@ -39,11 +38,11 @@ class EndstopCover : public cover::Cover, public Component { binary_sensor::BinarySensor *open_endstop_; binary_sensor::BinarySensor *close_endstop_; - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; uint32_t open_duration_; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; uint32_t close_duration_; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; uint32_t max_duration_{UINT32_MAX}; Trigger<> *prev_command_trigger_{nullptr}; diff --git a/esphome/components/ens210/ens210.cpp b/esphome/components/ens210/ens210.cpp index 98a300f5d7..8bee9bfb18 100644 --- a/esphome/components/ens210/ens210.cpp +++ b/esphome/components/ens210/ens210.cpp @@ -136,8 +136,6 @@ void ENS210Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float ENS210Component::get_setup_priority() const { return setup_priority::DATA; } - void ENS210Component::update() { // Execute a single measurement if (!this->write_byte(ENS210_REGISTER_SENS_RUN, 0x00)) { diff --git a/esphome/components/ens210/ens210.h b/esphome/components/ens210/ens210.h index 0fb6ff634d..ae2bf81b5f 100644 --- a/esphome/components/ens210/ens210.h +++ b/esphome/components/ens210/ens210.h @@ -10,7 +10,6 @@ namespace ens210 { /// This class implements support for the ENS210 relative humidity and temperature i2c sensor. class ENS210Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override; void dump_config() override; void setup() override; void update() override; diff --git a/esphome/components/epaper_spi/display.py b/esphome/components/epaper_spi/display.py index 8cc7b2663c..13f66691b2 100644 --- a/esphome/components/epaper_spi/display.py +++ b/esphome/components/epaper_spi/display.py @@ -76,50 +76,42 @@ def model_schema(config): model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s") ) cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required - return ( - display.FULL_DISPLAY_SCHEMA.extend( - spi.spi_device_schema( - cs_pin_required=False, - default_mode="MODE0", - default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), - ) - ) - .extend( - { - model.option(pin): pins.gpio_output_pin_schema - for pin in (CONF_RESET_PIN, CONF_CS_PIN, CONF_BUSY_PIN) - } - ) - .extend( - { - cv.Optional(CONF_ROTATION, default=0): validate_rotation, - cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), - cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( - update_interval, cv.Range(min=minimum_update_interval) - ), - cv.Optional(CONF_TRANSFORM): cv.Schema( - { - cv.Required(CONF_MIRROR_X): cv.boolean, - cv.Required(CONF_MIRROR_Y): cv.boolean, - } - ), - cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), - model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, - cv.GenerateID(): cv.declare_id(class_name), - cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), - cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, - model.option(CONF_ENABLE_PIN): cv.ensure_list( - pins.gpio_output_pin_schema - ), - model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( - map_sequence - ), - model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( - cv.positive_time_period_milliseconds, - cv.Range(max=core.TimePeriod(milliseconds=500)), - ), - } + return display.FULL_DISPLAY_SCHEMA.extend( + spi.spi_device_schema( + cs_pin_required=False, + default_mode="MODE0", + default_data_rate=model.get_default(CONF_DATA_RATE, 10_000_000), ) + ).extend( + { + cv.Optional(CONF_ROTATION, default=0): validate_rotation, + cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), + cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All( + update_interval, cv.Range(min=minimum_update_interval) + ), + cv.Optional(CONF_TRANSFORM): cv.Schema( + { + cv.Required(CONF_MIRROR_X): cv.boolean, + cv.Required(CONF_MIRROR_Y): cv.boolean, + } + ), + cv.Optional(CONF_FULL_UPDATE_EVERY, default=1): cv.int_range(1, 255), + model.option(CONF_BUSY_PIN): pins.gpio_input_pin_schema, + model.option(CONF_CS_PIN): pins.gpio_output_pin_schema, + model.option(CONF_DC_PIN, fallback=None): pins.gpio_output_pin_schema, + model.option(CONF_RESET_PIN): pins.gpio_output_pin_schema, + cv.GenerateID(): cv.declare_id(class_name), + cv.GenerateID(CONF_INIT_SEQUENCE_ID): cv.declare_id(cg.uint8), + cv_dimensions(CONF_DIMENSIONS): DIMENSION_SCHEMA, + model.option(CONF_ENABLE_PIN): cv.ensure_list(pins.gpio_output_pin_schema), + model.option(CONF_INIT_SEQUENCE, cv.UNDEFINED): cv.ensure_list( + map_sequence + ), + model.option(CONF_RESET_DURATION, cv.UNDEFINED): cv.All( + cv.positive_time_period_milliseconds, + cv.Range(max=core.TimePeriod(milliseconds=500)), + ), + } ) diff --git a/esphome/components/epaper_spi/epaper_spi.cpp b/esphome/components/epaper_spi/epaper_spi.cpp index db803305a5..ae1923a916 100644 --- a/esphome/components/epaper_spi/epaper_spi.cpp +++ b/esphome/components/epaper_spi/epaper_spi.cpp @@ -182,7 +182,9 @@ void EPaperBase::process_state_() { this->set_state_(EPaperState::RESET); break; case EPaperState::INITIALISE: - this->initialise(this->update_count_ != 0); + if (!this->initialise(this->update_count_ != 0)) { + return; // Not done yet, come back next loop + } this->set_state_(EPaperState::TRANSFER_DATA); break; case EPaperState::TRANSFER_DATA: @@ -239,11 +241,9 @@ void EPaperBase::start_data_() { void EPaperBase::on_safe_shutdown() { this->deep_sleep(); } -void EPaperBase::initialise(bool partial) { +void EPaperBase::send_init_sequence_(const uint8_t *sequence, size_t length) { size_t index = 0; - auto *sequence = this->init_sequence_; - auto length = this->init_sequence_length_; while (index != length) { if (length - index < 2) { this->mark_failed(LOG_STR("Malformed init sequence")); @@ -266,6 +266,11 @@ void EPaperBase::initialise(bool partial) { } } +bool EPaperBase::initialise(bool partial) { + this->send_init_sequence_(this->init_sequence_, this->init_sequence_length_); + return true; +} + /** * Check and rotate coordinates based on the transform flags. * @param x diff --git a/esphome/components/epaper_spi/epaper_spi.h b/esphome/components/epaper_spi/epaper_spi.h index 521543f026..a8c2fe9b56 100644 --- a/esphome/components/epaper_spi/epaper_spi.h +++ b/esphome/components/epaper_spi/epaper_spi.h @@ -115,7 +115,8 @@ class EPaperBase : public Display, bool is_idle_() const; void setup_pins_() const; virtual bool reset(); - virtual void initialise(bool partial); + virtual bool initialise(bool partial); + void send_init_sequence_(const uint8_t *sequence, size_t length); void wait_for_idle_(bool should_wait); bool init_buffer_(size_t buffer_length); bool rotate_coordinates_(int &x, int &y); diff --git a/esphome/components/epaper_spi/epaper_waveshare.cpp b/esphome/components/epaper_spi/epaper_waveshare.cpp index 8d382d86e7..7a7b4e22d3 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.cpp +++ b/esphome/components/epaper_spi/epaper_waveshare.cpp @@ -4,7 +4,7 @@ namespace esphome::epaper_spi { static const char *const TAG = "epaper_spi.waveshare"; -void EpaperWaveshare::initialise(bool partial) { +bool EpaperWaveshare::initialise(bool partial) { EPaperBase::initialise(partial); if (partial) { this->cmd_data(0x32, this->partial_lut_, this->partial_lut_length_); @@ -17,6 +17,7 @@ void EpaperWaveshare::initialise(bool partial) { this->cmd_data(0x3C, {0x05}); } this->send_red_ = true; + return true; } void EpaperWaveshare::set_window() { diff --git a/esphome/components/epaper_spi/epaper_waveshare.h b/esphome/components/epaper_spi/epaper_waveshare.h index 6b894cfd09..15fb575ca0 100644 --- a/esphome/components/epaper_spi/epaper_waveshare.h +++ b/esphome/components/epaper_spi/epaper_waveshare.h @@ -18,7 +18,7 @@ class EpaperWaveshare : public EPaperMono { partial_lut_length_(partial_lut_length) {} protected: - void initialise(bool partial) override; + bool initialise(bool partial) override; void set_window() override; void refresh_screen(bool partial) override; void deep_sleep() override; diff --git a/esphome/components/es8156/audio_dac.py b/esphome/components/es8156/audio_dac.py index b9d8eae6b0..c5fb6096da 100644 --- a/esphome/components/es8156/audio_dac.py +++ b/esphome/components/es8156/audio_dac.py @@ -2,7 +2,8 @@ import esphome.codegen as cg from esphome.components import i2c from esphome.components.audio_dac import AudioDac import esphome.config_validation as cv -from esphome.const import CONF_ID +from esphome.const import CONF_AUDIO_DAC, CONF_BITS_PER_SAMPLE, CONF_ID +import esphome.final_validate as fv CODEOWNERS = ["@kbx81"] DEPENDENCIES = ["i2c"] @@ -21,6 +22,29 @@ CONFIG_SCHEMA = ( ) +def _final_validate(config): + full_config = fv.full_config.get() + + # Check all speaker configurations for ones that reference this es8156 + speaker_configs = full_config.get("speaker", []) + for speaker_config in speaker_configs: + audio_dac_id = speaker_config.get(CONF_AUDIO_DAC) + if ( + audio_dac_id is not None + and audio_dac_id == config[CONF_ID] + and (bits_per_sample := speaker_config.get(CONF_BITS_PER_SAMPLE)) + is not None + and bits_per_sample > 24 + ): + raise cv.Invalid( + f"ES8156 does not support more than 24 bits per sample. " + f"The speaker referencing this audio_dac has bits_per_sample set to {bits_per_sample}." + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/es8156/es8156.cpp b/esphome/components/es8156/es8156.cpp index e84252efe2..961dc24b29 100644 --- a/esphome/components/es8156/es8156.cpp +++ b/esphome/components/es8156/es8156.cpp @@ -17,24 +17,61 @@ static const char *const TAG = "es8156"; } void ES8156::setup() { + // REG02 MODE CONFIG 1: Enable software mode for I2C control of volume/mute + // Bit 2: SOFT_MODE_SEL=1 (software mode enabled) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG02_SCLK_MODE, 0x04)); + + // Analog system configuration (active-low power down bits, active-high enables) + // REG20 ANALOG SYSTEM: Configure analog signal path ES8156_ERROR_FAILED(this->write_byte(ES8156_REG20_ANALOG_SYS1, 0x2A)); + + // REG21 ANALOG SYSTEM: VSEL=0x1C (bias level ~120%), normal VREF ramp speed ES8156_ERROR_FAILED(this->write_byte(ES8156_REG21_ANALOG_SYS2, 0x3C)); + + // REG22 ANALOG SYSTEM: Line out mode (HPSW=0), OUT_MUTE=0 (not muted) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG22_ANALOG_SYS3, 0x00)); + + // REG24 ANALOG SYSTEM: Low power mode for VREFBUF, HPCOM, DACVRP; DAC normal power + // Bits 2:0 = 0x07: LPVREFBUF=1, LPHPCOM=1, LPDACVRP=1, LPDAC=0 ES8156_ERROR_FAILED(this->write_byte(ES8156_REG24_ANALOG_LP, 0x07)); + + // REG23 ANALOG SYSTEM: Lowest bias (IBIAS_SW=0), VMIDLVL=VDDA/2, normal impedance ES8156_ERROR_FAILED(this->write_byte(ES8156_REG23_ANALOG_SYS4, 0x00)); + // Timing and interface configuration + // REG0A/0B TIME CONTROL: Fast state machine transitions ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0A_TIME_CONTROL1, 0x01)); ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0B_TIME_CONTROL2, 0x01)); + + // REG11 SDP INTERFACE CONFIG: Default I2S format (24-bit, I2S mode) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG11_DAC_SDP, 0x00)); + + // REG19 EQ CONTROL 1: EQ disabled (EQ_ON=0), EQ_BAND_NUM=2 ES8156_ERROR_FAILED(this->write_byte(ES8156_REG19_EQ_CONTROL1, 0x20)); + // REG0D P2S CONTROL: Parallel-to-serial converter settings ES8156_ERROR_FAILED(this->write_byte(ES8156_REG0D_P2S_CONTROL, 0x14)); + + // REG09 MISC CONTROL 2: Default settings ES8156_ERROR_FAILED(this->write_byte(ES8156_REG09_MISC_CONTROL2, 0x00)); + + // REG18 MISC CONTROL 3: Stereo channel routing, no inversion + // Bits 5:4 CHN_CROSS: 0=L→L/R→R, 1=L to both, 2=R to both, 3=swap L/R + // Bits 3:2: LCH_INV/RCH_INV channel inversion ES8156_ERROR_FAILED(this->write_byte(ES8156_REG18_MISC_CONTROL3, 0x00)); + + // REG08 CLOCK OFF: Enable all internal clocks (0x3F = all clock gates open) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG08_CLOCK_ON_OFF, 0x3F)); + + // REG00 RESET CONTROL: Reset sequence + // First: RST_DIG=1 (assert digital reset) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x02)); + // Then: CSM_ON=1 (enable chip state machine), RST_DIG=1 ES8156_ERROR_FAILED(this->write_byte(ES8156_REG00_RESET, 0x03)); + + // REG25 ANALOG SYSTEM: Power up analog blocks + // VMIDSEL=2 (normal VMID operation), PDN_ANA=0, ENREFR=0, ENHPCOM=0 + // PDN_DACVREFGEN=0, PDN_VREFBUF=0, PDN_DAC=0 (all enabled) ES8156_ERROR_FAILED(this->write_byte(ES8156_REG25_ANALOG_SYS5, 0x20)); } diff --git a/esphome/components/esp32/__init__.py b/esphome/components/esp32/__init__.py index fccf0ed09f..90f6035aba 100644 --- a/esphome/components/esp32/__init__.py +++ b/esphome/components/esp32/__init__.py @@ -46,13 +46,15 @@ from esphome.coroutine import CoroPriority, coroutine_with_priority import esphome.final_validate as fv from esphome.helpers import copy_file_if_changed, write_file_if_changed from esphome.types import ConfigType -from esphome.writer import clean_cmake_cache +from esphome.writer import clean_cmake_cache, rmtree from .boards import BOARDS, STANDARD_BOARDS from .const import ( # noqa + KEY_ARDUINO_LIBRARIES, KEY_BOARD, KEY_COMPONENTS, KEY_ESP32, + KEY_EXCLUDE_COMPONENTS, KEY_EXTRA_BUILD_FILES, KEY_FLASH_SIZE, KEY_FULL_CERT_BUNDLE, @@ -86,12 +88,18 @@ IS_TARGET_PLATFORM = True CONF_ASSERTION_LEVEL = "assertion_level" CONF_COMPILER_OPTIMIZATION = "compiler_optimization" CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features" +CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components" CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert" CONF_ENABLE_OTA_ROLLBACK = "enable_ota_rollback" CONF_EXECUTE_FROM_PSRAM = "execute_from_psram" CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision" CONF_RELEASE = "release" +ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32" +ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}" +ARDUINO_LIBS_NAME = f"{ARDUINO_FRAMEWORK_NAME}-libs" +ARDUINO_LIBS_PKG = f"pioarduino/{ARDUINO_LIBS_NAME}" + LOG_LEVELS_IDF = [ "NONE", "ERROR", @@ -114,6 +122,204 @@ COMPILER_OPTIMIZATIONS = { "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", } +# ESP-IDF components excluded by default to reduce compile time. +# Components can be re-enabled by calling include_builtin_idf_component() in to_code(). +# +# Cannot be excluded (dependencies of required components): +# - "console": espressif/mdns unconditionally depends on it +# - "sdmmc": driver -> esp_driver_sdmmc -> sdmmc dependency chain +DEFAULT_EXCLUDED_IDF_COMPONENTS = ( + "cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing + "driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers + "esp_adc", # ADC driver - only needed by adc component + "esp_driver_dac", # DAC driver - only needed by esp32_dac component + "esp_driver_i2s", # I2S driver - only needed by i2s_audio component + "esp_driver_mcpwm", # MCPWM driver - ESPHome doesn't use motor control PWM + "esp_driver_rmt", # RMT driver - only needed by remote_transmitter/receiver, neopixelbus + "esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch + "esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component + "esp_eth", # Ethernet driver - only needed by ethernet component + "esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality + "esp_http_client", # HTTP client - only needed by http_request component + "esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation + "esp_https_server", # HTTPS server - ESPHome has its own web server + "esp_lcd", # LCD controller drivers - only needed by display component + "esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API + "espcoredump", # Core dump support - ESPHome has its own debug component + "fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage + "mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation + "openthread", # Thread protocol - only needed by openthread component + "perfmon", # Xtensa performance monitor - ESPHome has its own debug component + "protocomm", # Protocol communication for provisioning - unused by ESPHome + "spiffs", # SPIFFS filesystem - ESPHome doesn't use filesystem storage (IDF only) + "ulp", # ULP coprocessor - not currently used by any ESPHome component + "unity", # Unit testing framework - ESPHome doesn't use IDF's testing + "wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused + "wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation +) + +# Additional IDF managed components to exclude for Arduino framework builds +# These are pulled in by the Arduino framework's idf_component.yml but not used by ESPHome +# Note: Component names include the namespace prefix (e.g., "espressif__cbor") because +# that's how managed components are registered in the IDF build system +# List includes direct dependencies from arduino-esp32/idf_component.yml +# plus transitive dependencies from RainMaker/Insights (except espressif/mdns which we need) +ARDUINO_EXCLUDED_IDF_COMPONENTS = ( + "chmorgan__esp-libhelix-mp3", # MP3 decoder - not used + "espressif__cbor", # CBOR library - only used by RainMaker/Insights + "espressif__esp-dsp", # DSP library - not used + "espressif__esp-modbus", # Modbus - ESPHome has its own + "espressif__esp-sr", # Speech recognition - not used + "espressif__esp-zboss-lib", # Zigbee ZBOSS library - not used + "espressif__esp-zigbee-lib", # Zigbee library - not used + "espressif__esp_diag_data_store", # Diagnostics - not used + "espressif__esp_diagnostics", # Diagnostics - not used + "espressif__esp_hosted", # ESP hosted - only for ESP32-P4 + "espressif__esp_insights", # ESP Insights - not used + "espressif__esp_modem", # Modem library - not used + "espressif__esp_rainmaker", # RainMaker - not used + "espressif__esp_rcp_update", # RCP update - RainMaker transitive dep + "espressif__esp_schedule", # Schedule - RainMaker transitive dep + "espressif__esp_secure_cert_mgr", # Secure cert - RainMaker transitive dep + "espressif__esp_wifi_remote", # WiFi remote - only for ESP32-P4 + "espressif__json_generator", # JSON generator - RainMaker transitive dep + "espressif__json_parser", # JSON parser - RainMaker transitive dep + "espressif__lan867x", # Ethernet PHY - ESPHome uses ESP-IDF ethernet directly + "espressif__libsodium", # Crypto - ESPHome uses its own noise-c library + "espressif__network_provisioning", # Network provisioning - not used + "espressif__qrcode", # QR code - not used + "espressif__rmaker_common", # RainMaker common - not used + "joltwallet__littlefs", # LittleFS - ESPHome doesn't use filesystem +) + +# Mapping of Arduino libraries to IDF managed components they require +# When an Arduino library is enabled via cg.add_library(), these components +# are automatically un-stubbed from ARDUINO_EXCLUDED_IDF_COMPONENTS. +# +# Note: Some libraries (Matter, LittleFS, ESP_SR, WiFiProv, ArduinoOTA) already have +# conditional maybe_add_component() calls in arduino-esp32/CMakeLists.txt that handle +# their managed component dependencies. Our mapping is primarily needed for libraries +# that don't have such conditionals (Ethernet, PPP, Zigbee, RainMaker, Insights, etc.) +# and to ensure the stubs are removed from our idf_component.yml overrides. +ARDUINO_LIBRARY_IDF_COMPONENTS: dict[str, tuple[str, ...]] = { + "BLE": ("esp_driver_gptimer",), + "BluetoothSerial": ("esp_driver_gptimer",), + "ESP_HostedOTA": ("espressif__esp_hosted", "espressif__esp_wifi_remote"), + "ESP_SR": ("espressif__esp-sr",), + "Ethernet": ("espressif__lan867x",), + "FFat": ("fatfs",), + "Insights": ( + "espressif__cbor", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", # Transitive dep from esp_insights + ), + "LittleFS": ("joltwallet__littlefs",), + "Matter": ("espressif__esp_matter",), + "PPP": ("espressif__esp_modem",), + "RainMaker": ( + # Direct deps from idf_component.yml + "espressif__cbor", + "espressif__esp_rainmaker", + "espressif__esp_insights", + "espressif__esp_diagnostics", + "espressif__esp_diag_data_store", + "espressif__rmaker_common", + "espressif__qrcode", + # Transitive deps from esp_rainmaker + "espressif__esp_rcp_update", + "espressif__esp_schedule", + "espressif__esp_secure_cert_mgr", + "espressif__json_generator", + "espressif__json_parser", + "espressif__network_provisioning", + ), + "SD": ("fatfs",), + "SD_MMC": ("fatfs",), + "SPIFFS": ("spiffs",), + "WiFiProv": ("espressif__network_provisioning", "espressif__qrcode"), + "Zigbee": ("espressif__esp-zigbee-lib", "espressif__esp-zboss-lib"), +} + +# Arduino library to Arduino library dependencies +# When enabling one library, also enable its dependencies +# Kconfig "select" statements don't work with CONFIG_ARDUINO_SELECTIVE_COMPILATION +ARDUINO_LIBRARY_DEPENDENCIES: dict[str, tuple[str, ...]] = { + "Ethernet": ("Network",), + "WiFi": ("Network",), +} + + +def _idf_component_stub_name(component: str) -> str: + """Get stub directory name from IDF component name. + + Component names are typically namespace__name (e.g., espressif__cbor). + Returns just the name part (e.g., cbor). If no namespace is present, + returns the original component name. + """ + _prefix, sep, suffix = component.partition("__") + return suffix if sep else component + + +def _idf_component_dep_name(component: str) -> str: + """Convert IDF component name to dependency format. + + Converts espressif__cbor to espressif/cbor. + """ + return component.replace("__", "/") + + +# Arduino libraries to disable by default when using Arduino framework +# ESPHome uses ESP-IDF APIs directly; we only need the Arduino core +# (HardwareSerial, Print, Stream, GPIO functions which are always compiled) +# Components use cg.add_library() which auto-enables any they need +# This list must match ARDUINO_ALL_LIBRARIES from arduino-esp32/CMakeLists.txt +ARDUINO_DISABLED_LIBRARIES: frozenset[str] = frozenset( + { + "ArduinoOTA", + "AsyncUDP", + "BLE", + "BluetoothSerial", + "DNSServer", + "EEPROM", + "ESP_HostedOTA", + "ESP_I2S", + "ESP_NOW", + "ESP_SR", + "ESPmDNS", + "Ethernet", + "FFat", + "FS", + "Hash", + "HTTPClient", + "HTTPUpdate", + "Insights", + "LittleFS", + "Matter", + "NetBIOS", + "Network", + "NetworkClientSecure", + "OpenThread", + "PPP", + "Preferences", + "RainMaker", + "SD", + "SD_MMC", + "SimpleBLE", + "SPI", + "SPIFFS", + "Ticker", + "Update", + "USB", + "WebServer", + "WiFi", + "WiFiProv", + "Wire", + "Zigbee", + } +) + # ESP32 (original) chip revision options # Setting minimum revision to 3.0 or higher: # - Reduces flash size by excluding workaround code for older chip bugs @@ -203,6 +409,15 @@ def set_core_data(config): ) CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} + # Initialize with default exclusions - components can call include_builtin_idf_component() + # to re-enable any they need + excluded = set(DEFAULT_EXCLUDED_IDF_COMPONENTS) + # Add Arduino-specific managed component exclusions when using Arduino framework + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + excluded.update(ARDUINO_EXCLUDED_IDF_COMPONENTS) + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded + # Initialize Arduino library tracking - cg.add_library() auto-enables libraries + CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set() CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( config[CONF_FRAMEWORK][CONF_VERSION] ) @@ -328,6 +543,48 @@ def add_idf_component( } +def exclude_builtin_idf_component(name: str) -> None: + """Exclude an ESP-IDF component from the build. + + This reduces compile time by skipping components that are not needed. + The component will be passed to ESP-IDF's EXCLUDE_COMPONENTS cmake variable. + + Note: Components that are dependencies of other required components + cannot be excluded - ESP-IDF will still build them. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].add(name) + + +def include_builtin_idf_component(name: str) -> None: + """Remove an ESP-IDF component from the exclusion list. + + Call this from components that need an ESP-IDF component that is + excluded by default in DEFAULT_EXCLUDED_IDF_COMPONENTS. This ensures the + component will be built when needed. + """ + CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS].discard(name) + + +def _enable_arduino_library(name: str) -> None: + """Enable an Arduino library that is disabled by default. + + This is called automatically by CORE.add_library() when a component adds + an Arduino library via cg.add_library(). Components should not call this + directly - just use cg.add_library("LibName", None). + + Args: + name: The library name (e.g., "Wire", "SPI", "WiFi") + """ + enabled_libs: set[str] = CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] + enabled_libs.add(name) + # Also enable any required Arduino library dependencies + for dep_lib in ARDUINO_LIBRARY_DEPENDENCIES.get(name, ()): + enabled_libs.add(dep_lib) + # Also enable any required IDF components + for idf_component in ARDUINO_LIBRARY_IDF_COMPONENTS.get(name, ()): + include_builtin_idf_component(idf_component) + + def add_extra_script(stage: str, filename: str, path: Path): """Add an extra script to the project.""" key = f"{stage}:{filename}" @@ -347,14 +604,12 @@ def add_extra_build_file(filename: str, path: Path) -> bool: def _format_framework_arduino_version(ver: cv.Version) -> str: - # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to - # a PIO pioarduino/framework-arduinoespressif32 value # 3.3.6+ changed filename from esp32-{ver}.zip to esp32-core-{ver}.tar.xz if ver >= cv.Version(3, 3, 6): filename = f"esp32-core-{ver}.tar.xz" else: filename = f"esp32-{ver}.zip" - return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" + return f"{ARDUINO_FRAMEWORK_PKG}@https://github.com/espressif/arduino-esp32/releases/download/{ver}/{filename}" def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: @@ -454,7 +709,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = { PLATFORM_VERSION_LOOKUP = { "recommended": cv.Version(55, 3, 36), "latest": cv.Version(55, 3, 36), - "dev": cv.Version(55, 3, 36), + "dev": "https://github.com/pioarduino/platform-espressif32.git#develop", } @@ -489,9 +744,7 @@ def _check_versions(config): CONF_SOURCE, _format_framework_arduino_version(version) ) if _is_framework_url(value[CONF_SOURCE]): - value[CONF_SOURCE] = ( - f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}" - ) + value[CONF_SOURCE] = f"{ARDUINO_FRAMEWORK_PKG}@{value[CONF_SOURCE]}" else: if version < cv.Version(5, 0, 0): raise cv.Invalid("Only ESP-IDF 5.0+ is supported.") @@ -672,11 +925,26 @@ CONF_RINGBUF_IN_IRAM = "ringbuf_in_iram" CONF_HEAP_IN_IRAM = "heap_in_iram" CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size" CONF_USE_FULL_CERTIFICATE_BUNDLE = "use_full_certificate_bundle" +CONF_DISABLE_DEBUG_STUBS = "disable_debug_stubs" +CONF_DISABLE_OCD_AWARE = "disable_ocd_aware" +CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY = "disable_usb_serial_jtag_secondary" +CONF_DISABLE_DEV_NULL_VFS = "disable_dev_null_vfs" +CONF_DISABLE_MBEDTLS_PEER_CERT = "disable_mbedtls_peer_cert" +CONF_DISABLE_MBEDTLS_PKCS7 = "disable_mbedtls_pkcs7" +CONF_DISABLE_REGI2C_IN_IRAM = "disable_regi2c_in_iram" +CONF_DISABLE_FATFS = "disable_fatfs" # VFS requirement tracking -# Components that need VFS features can call require_vfs_select() or require_vfs_dir() +# Components that need VFS features can call require_vfs_*() functions KEY_VFS_SELECT_REQUIRED = "vfs_select_required" KEY_VFS_DIR_REQUIRED = "vfs_dir_required" +KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required" +# Feature requirement tracking - components can call require_* functions to re-enable +# These are stored in CORE.data[KEY_ESP32] dict +KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required" +KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required" +KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required" +KEY_FATFS_REQUIRED = "fatfs_required" def require_vfs_select() -> None: @@ -697,6 +965,15 @@ def require_vfs_dir() -> None: CORE.data[KEY_VFS_DIR_REQUIRED] = True +def require_vfs_termios() -> None: + """Mark that VFS termios support is required by a component. + + Call this from components that use terminal I/O functions (usb_serial_jtag_vfs_*, etc.). + This prevents CONFIG_VFS_SUPPORT_TERMIOS from being disabled. + """ + CORE.data[KEY_VFS_TERMIOS_REQUIRED] = True + + def require_full_certificate_bundle() -> None: """Request the full certificate bundle instead of the common-CAs-only bundle. @@ -709,6 +986,43 @@ def require_full_certificate_bundle() -> None: CORE.data[KEY_ESP32][KEY_FULL_CERT_BUNDLE] = True +def require_usb_serial_jtag_secondary() -> None: + """Mark that USB Serial/JTAG secondary console is required by a component. + + Call this from components (e.g., logger) that need USB Serial/JTAG console output. + This prevents CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG from being disabled. + """ + CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True + + +def require_mbedtls_peer_cert() -> None: + """Mark that mbedTLS peer certificate retention is required by a component. + + Call this from components that need access to the peer certificate after + the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE + from being disabled. + """ + CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True + + +def require_mbedtls_pkcs7() -> None: + """Mark that mbedTLS PKCS#7 support is required by a component. + + Call this from components that need PKCS#7 certificate validation. + This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled. + """ + CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True + + +def require_fatfs() -> None: + """Mark that FATFS support is required by a component. + + Call this from components that use FATFS (e.g., SD card, storage components). + This prevents FATFS from being disabled when disable_fatfs is set. + """ + CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True + + def _parse_idf_component(value: str) -> ConfigType: """Parse IDF component shorthand syntax like 'owner/component^version'""" # Match operator followed by version-like string (digit or *) @@ -793,6 +1107,19 @@ FRAMEWORK_SCHEMA = cv.Schema( cv.Optional( CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False ): cv.boolean, + cv.Optional( + CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, default=[] + ): cv.ensure_list(cv.string_strict), + cv.Optional(CONF_DISABLE_DEBUG_STUBS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_OCD_AWARE, default=True): cv.boolean, + cv.Optional( + CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY, default=True + ): cv.boolean, + cv.Optional(CONF_DISABLE_DEV_NULL_VFS, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_MBEDTLS_PEER_CERT, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_MBEDTLS_PKCS7, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_REGI2C_IN_IRAM, default=True): cv.boolean, + cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean, } ), cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list( @@ -842,8 +1169,8 @@ def _show_framework_migration_message(name: str, variant: str) -> None: + "(We've been warning about this change since ESPHome 2025.8.0)\n" + "\n" + "Why we made this change:\n" - + color(AnsiFore.GREEN, " ✨ Up to 40% smaller firmware binaries\n") - + color(AnsiFore.GREEN, " ⚡ 2-3x faster compile times\n") + + color(AnsiFore.GREEN, " ✨ Smaller firmware binaries\n") + + color(AnsiFore.GREEN, " ⚡ Faster compile times\n") + color(AnsiFore.GREEN, " 🚀 Better performance and newer features\n") + color(AnsiFore.GREEN, " 🔧 More actively maintained by ESPHome\n") + "\n" @@ -982,6 +1309,54 @@ def _configure_lwip_max_sockets(conf: dict) -> None: add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets) +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_exclude_components() -> None: + """Write EXCLUDE_COMPONENTS cmake arg after all components have registered exclusions.""" + if KEY_ESP32 not in CORE.data: + return + excluded = CORE.data[KEY_ESP32].get(KEY_EXCLUDE_COMPONENTS) + if excluded: + exclude_list = ";".join(sorted(excluded)) + cg.add_platformio_option( + "board_build.cmake_extra_args", f"-DEXCLUDE_COMPONENTS={exclude_list}" + ) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libs_stub(stubs_dir: Path, idf_ver: cv.Version) -> None: + """Write stub package to skip downloading precompiled Arduino libs.""" + stubs_dir.mkdir(parents=True, exist_ok=True) + write_file_if_changed( + stubs_dir / "package.json", + f'{{"name":"{ARDUINO_LIBS_NAME}","version":"{idf_ver.major}.{idf_ver.minor}.{idf_ver.patch}"}}', + ) + write_file_if_changed( + stubs_dir / "tools.json", + '{"packages":[{"platforms":[{"toolsDependencies":[]}],"tools":[]}]}', + ) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _write_arduino_libraries_sdkconfig() -> None: + """Write Arduino selective compilation sdkconfig after all components have added libraries. + + This must run at FINAL priority so that all components have had a chance to call + cg.add_library() which auto-enables Arduino libraries via _enable_arduino_library(). + """ + if KEY_ESP32 not in CORE.data: + return + # Enable Arduino selective compilation to disable unused Arduino libraries + # ESPHome uses ESP-IDF APIs directly; we only need the Arduino core + # (HardwareSerial, Print, Stream, GPIO functions which are always compiled) + # cg.add_library() auto-enables needed libraries; users can also add + # libraries via esphome: libraries: config which calls cg.add_library() + add_idf_sdkconfig_option("CONFIG_ARDUINO_SELECTIVE_COMPILATION", True) + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + for lib in ARDUINO_DISABLED_LIBRARIES: + # Enable if explicitly requested, disable otherwise + add_idf_sdkconfig_option(f"CONFIG_ARDUINO_SELECTIVE_{lib}", lib in enabled_libs) + + @coroutine_with_priority(CoroPriority.FINAL) async def _add_yaml_idf_components(components: list[ConfigType]): """Add IDF components from YAML config with final priority to override code-added components.""" @@ -1091,6 +1466,12 @@ async def to_code(config): "platform_packages", [_format_framework_espidf_version(idf_ver, None)], ) + # Use stub package to skip downloading precompiled libs + stubs_dir = CORE.relative_build_path("arduino_libs_stub") + cg.add_platformio_option( + "platform_packages", [f"{ARDUINO_LIBS_PKG}@file://{stubs_dir}"] + ) + CORE.add_job(_write_arduino_libs_stub, stubs_dir, idf_ver) # ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency if get_esp32_variant() == VARIANT_ESP32S2: @@ -1185,6 +1566,10 @@ async def to_code(config): # Disable dynamic log level control to save memory add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False) + # Disable per-tag log level filtering since dynamic level control is disabled above + # This saves ~250 bytes of RAM (tag cache) and associated code + add_idf_sdkconfig_option("CONFIG_LOG_TAG_LEVEL_IMPL_NONE", True) + # Reduce PHY TX power in the event of a brownout add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True) @@ -1195,6 +1580,11 @@ async def to_code(config): # Apply LWIP optimization settings advanced = conf[CONF_ADVANCED] + + # Re-include any IDF components the user explicitly requested + for component_name in advanced.get(CONF_INCLUDE_BUILTIN_IDF_COMPONENTS, []): + include_builtin_idf_component(component_name) + # DHCP server: only disable if explicitly set to false # WiFi component handles its own optimization when AP mode is not used # When using Arduino with Ethernet, DHCP server functions must be available @@ -1233,11 +1623,18 @@ async def to_code(config): add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) # Disable VFS support for termios (terminal I/O functions) - # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). + # USB Serial JTAG VFS functions require termios support. + # Components that need it (e.g., logger when USB_SERIAL_JTAG is supported but not selected + # as the logger output) call require_vfs_termios(). # Saves approximately 1.8KB of flash when disabled (default). - add_idf_sdkconfig_option( - "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] - ) + if CORE.data.get(KEY_VFS_TERMIOS_REQUIRED, False): + # Component requires VFS termios - force enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_TERMIOS", True) + else: + # No component needs it - allow user to control (default: disabled) + add_idf_sdkconfig_option( + "CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS] + ) # Disable VFS support for select() with file descriptors # ESPHome only uses select() with sockets via lwip_select(), which still works. @@ -1316,6 +1713,61 @@ async def to_code(config): add_idf_sdkconfig_option(f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True) + # Disable OpenOCD debug stubs to save code size + # These are used for on-chip debugging with OpenOCD/JTAG, rarely needed for ESPHome + if advanced[CONF_DISABLE_DEBUG_STUBS]: + add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_STUBS_ENABLE", False) + + # Disable OCD-aware exception handlers + # When enabled, the panic handler detects JTAG debugger and halts instead of resetting + # Most ESPHome users don't use JTAG debugging + if advanced[CONF_DISABLE_OCD_AWARE]: + add_idf_sdkconfig_option("CONFIG_ESP_DEBUG_OCDAWARE", False) + + # Disable USB Serial/JTAG secondary console + # Components like logger can call require_usb_serial_jtag_secondary() to re-enable + if CORE.data[KEY_ESP32].get(KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED, False): + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_USB_SERIAL_JTAG", True) + elif advanced[CONF_DISABLE_USB_SERIAL_JTAG_SECONDARY]: + add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_SECONDARY_NONE", True) + + # Disable /dev/null VFS initialization + # ESPHome doesn't typically need /dev/null + if advanced[CONF_DISABLE_DEV_NULL_VFS]: + add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False) + + # Disable keeping peer certificate after TLS handshake + # Saves ~4KB heap per connection, but prevents certificate inspection after handshake + # Components that need it can call require_mbedtls_peer_cert() + if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False): + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True) + elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]: + add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False) + + # Disable PKCS#7 support in mbedTLS + # Only needed for specific certificate validation scenarios + # Components that need it can call require_mbedtls_pkcs7() + if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False): + # Component called require_mbedtls_pkcs7() - enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True) + elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]: + add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False) + + # Disable regi2c control functions in IRAM + # Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled + if advanced[CONF_DISABLE_REGI2C_IN_IRAM]: + add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False) + + # Disable FATFS support + # Components that need FATFS (SD card, etc.) can call require_fatfs() + if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False): + # Component called require_fatfs() - enable regardless of user setting + add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", False) + add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 2) + elif advanced[CONF_DISABLE_FATFS]: + add_idf_sdkconfig_option("CONFIG_FATFS_LFN_NONE", True) + add_idf_sdkconfig_option("CONFIG_FATFS_VOLUME_COUNT", 0) + for name, value in conf[CONF_SDKCONFIG_OPTIONS].items(): add_idf_sdkconfig_option(name, RawSdkconfigValue(value)) @@ -1324,6 +1776,16 @@ async def to_code(config): if conf[CONF_COMPONENTS]: CORE.add_job(_add_yaml_idf_components, conf[CONF_COMPONENTS]) + # Write EXCLUDE_COMPONENTS at FINAL priority after all components have had + # a chance to call include_builtin_idf_component() to re-enable components they need. + # Default exclusions are added in set_core_data() during config validation. + CORE.add_job(_write_exclude_components) + + # Write Arduino selective compilation sdkconfig at FINAL priority after all + # components have had a chance to call cg.add_library() to enable libraries they need. + if conf[CONF_TYPE] == FRAMEWORK_ARDUINO: + CORE.add_job(_write_arduino_libraries_sdkconfig) + APP_PARTITION_SIZES = { "2MB": 0x0C0000, # 768 KB @@ -1404,11 +1866,49 @@ def _write_sdkconfig(): def _write_idf_component_yml(): yml_path = CORE.relative_build_path("src/idf_component.yml") + dependencies: dict[str, dict] = {} + + # For Arduino builds, override unused managed components from the Arduino framework + # by pointing them to empty stub directories using override_path + # This prevents the IDF component manager from downloading the real components + if CORE.using_arduino: + # Determine which IDF components are needed by enabled Arduino libraries + enabled_libs = CORE.data[KEY_ESP32].get(KEY_ARDUINO_LIBRARIES, set()) + required_idf_components = { + comp + for lib in enabled_libs + for comp in ARDUINO_LIBRARY_IDF_COMPONENTS.get(lib, ()) + } + + # Only stub components that are not required by any enabled Arduino library + components_to_stub = ( + set(ARDUINO_EXCLUDED_IDF_COMPONENTS) - required_idf_components + ) + + stubs_dir = CORE.relative_build_path("component_stubs") + stubs_dir.mkdir(exist_ok=True) + for component_name in components_to_stub: + # Create stub directory with minimal CMakeLists.txt + stub_path = stubs_dir / _idf_component_stub_name(component_name) + stub_path.mkdir(exist_ok=True) + stub_cmake = stub_path / "CMakeLists.txt" + if not stub_cmake.exists(): + stub_cmake.write_text("idf_component_register()\n") + dependencies[_idf_component_dep_name(component_name)] = { + "version": "*", + "override_path": str(stub_path), + } + + # Remove stubs for components that are now required by enabled libraries + for component_name in required_idf_components: + stub_path = stubs_dir / _idf_component_stub_name(component_name) + if stub_path.exists(): + rmtree(stub_path) + if CORE.data[KEY_ESP32][KEY_COMPONENTS]: components: dict = CORE.data[KEY_ESP32][KEY_COMPONENTS] - dependencies = {} for name, component in components.items(): - dependency = {} + dependency: dict[str, str] = {} if component[KEY_REF]: dependency["version"] = component[KEY_REF] if component[KEY_REPO]: @@ -1416,9 +1916,8 @@ def _write_idf_component_yml(): if component[KEY_PATH]: dependency["path"] = component[KEY_PATH] dependencies[name] = dependency - contents = yaml_util.dump({"dependencies": dependencies}) - else: - contents = "" + + contents = yaml_util.dump({"dependencies": dependencies}) if dependencies else "" if write_file_if_changed(yml_path, contents): dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): diff --git a/esphome/components/esp32/const.py b/esphome/components/esp32/const.py index 9f8165818b..7874c1c759 100644 --- a/esphome/components/esp32/const.py +++ b/esphome/components/esp32/const.py @@ -6,6 +6,8 @@ KEY_FLASH_SIZE = "flash_size" KEY_VARIANT = "variant" KEY_SDKCONFIG_OPTIONS = "sdkconfig_options" KEY_COMPONENTS = "components" +KEY_EXCLUDE_COMPONENTS = "exclude_components" +KEY_ARDUINO_LIBRARIES = "arduino_libraries" KEY_REPO = "repo" KEY_REF = "ref" KEY_REFRESH = "refresh" diff --git a/esphome/components/esp32/preferences.cpp b/esphome/components/esp32/preferences.cpp index 4e0bb68133..7d5af023b4 100644 --- a/esphome/components/esp32/preferences.cpp +++ b/esphome/components/esp32/preferences.cpp @@ -203,10 +203,11 @@ class ESP32Preferences : public ESPPreferences { } }; +static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ESP32Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace esp32 diff --git a/esphome/components/esp32_ble/ble_advertising.h b/esphome/components/esp32_ble/ble_advertising.h index d7f1eeac9d..3cfa6f548a 100644 --- a/esphome/components/esp32_ble/ble_advertising.h +++ b/esphome/components/esp32_ble/ble_advertising.h @@ -10,20 +10,11 @@ #ifdef USE_ESP32 #ifdef USE_ESP32_BLE_ADVERTISING -#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID -#include -#endif #include #include namespace esphome::esp32_ble { -using raw_adv_data_t = struct { - uint8_t *data; - size_t length; - esp_power_level_t power_level; -}; - class ESPBTUUID; class BLEAdvertising { diff --git a/esphome/components/esp32_ble_beacon/__init__.py b/esphome/components/esp32_ble_beacon/__init__.py index ba5ae4331c..04c783980d 100644 --- a/esphome/components/esp32_ble_beacon/__init__.py +++ b/esphome/components/esp32_ble_beacon/__init__.py @@ -53,8 +53,10 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_MEASURED_POWER, default=-59): cv.int_range( min=-128, max=0 ), - cv.Optional(CONF_TX_POWER, default="3dBm"): cv.All( - cv.decibel, cv.enum(esp32_ble.TX_POWER_LEVELS, int=True) + cv.OnlyWithout(CONF_TX_POWER, "esp32_hosted", default="3dBm"): cv.All( + cv.conflicts_with_component("esp32_hosted"), + cv.decibel, + cv.enum(esp32_ble.TX_POWER_LEVELS, int=True), ), } ).extend(cv.COMPONENT_SCHEMA), @@ -82,7 +84,10 @@ async def to_code(config): cg.add(var.set_min_interval(config[CONF_MIN_INTERVAL])) cg.add(var.set_max_interval(config[CONF_MAX_INTERVAL])) cg.add(var.set_measured_power(config[CONF_MEASURED_POWER])) - cg.add(var.set_tx_power(config[CONF_TX_POWER])) + + # TX power control only available on native Bluetooth (not ESP-Hosted) + if CONF_TX_POWER in config: + cg.add(var.set_tx_power(config[CONF_TX_POWER])) cg.add_define("USE_ESP32_BLE_ADVERTISING") diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp index f2aa7e762e..093273b399 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.cpp @@ -36,11 +36,16 @@ void ESP32BLEBeacon::dump_config() { } } *bpos = '\0'; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d" ", TX Power: %ddBm", uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_, (this->tx_power_ * 3) - 12); +#else + ESP_LOGCONFIG(TAG, " UUID: %s, Major: %u, Minor: %u, Min Interval: %ums, Max Interval: %ums, Measured Power: %d", + uuid, this->major_, this->minor_, this->min_interval_, this->max_interval_, this->measured_power_); +#endif } float ESP32BLEBeacon::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } @@ -74,11 +79,14 @@ void ESP32BLEBeacon::on_advertise_() { ibeacon_adv_data.ibeacon_vendor.major = byteswap(this->major_); ibeacon_adv_data.ibeacon_vendor.measured_power = static_cast(this->measured_power_); + esp_err_t err; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID ESP_LOGD(TAG, "Setting BLE TX power"); - esp_err_t err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); + err = esp_ble_tx_power_set(ESP_BLE_PWR_TYPE_ADV, this->tx_power_); if (err != ESP_OK) { ESP_LOGW(TAG, "esp_ble_tx_power_set failed: %s", esp_err_to_name(err)); } +#endif err = esp_ble_gap_config_adv_data_raw((uint8_t *) &ibeacon_adv_data, sizeof(ibeacon_adv_data)); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gap_config_adv_data_raw failed: %s", esp_err_to_name(err)); diff --git a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h index 05afdc7379..7a0424f3aa 100644 --- a/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h +++ b/esphome/components/esp32_ble_beacon/esp32_ble_beacon.h @@ -48,7 +48,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented void set_min_interval(uint16_t val) { this->min_interval_ = val; } void set_max_interval(uint16_t val) { this->max_interval_ = val; } void set_measured_power(int8_t val) { this->measured_power_ = val; } +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID void set_tx_power(esp_power_level_t val) { this->tx_power_ = val; } +#endif void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; protected: @@ -60,7 +62,9 @@ class ESP32BLEBeacon : public Component, public GAPEventHandler, public Parented uint16_t min_interval_{}; uint16_t max_interval_{}; int8_t measured_power_{}; +#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID esp_power_level_t tx_power_{}; +#endif esp_ble_adv_params_t ble_adv_params_; bool advertising_{false}; }; diff --git a/esphome/components/esp32_camera/esp32_camera.cpp b/esphome/components/esp32_camera/esp32_camera.cpp index 5466d2e7ef..cfe06b1673 100644 --- a/esphome/components/esp32_camera/esp32_camera.cpp +++ b/esphome/components/esp32_camera/esp32_camera.cpp @@ -235,8 +235,6 @@ void ESP32Camera::loop() { this->single_requesters_ = 0; } -float ESP32Camera::get_setup_priority() const { return setup_priority::DATA; } - /* ---------------- constructors ---------------- */ ESP32Camera::ESP32Camera() { this->config_.pin_pwdn = -1; diff --git a/esphome/components/esp32_camera/esp32_camera.h b/esphome/components/esp32_camera/esp32_camera.h index e97eb27c70..eea93b7e01 100644 --- a/esphome/components/esp32_camera/esp32_camera.h +++ b/esphome/components/esp32_camera/esp32_camera.h @@ -159,7 +159,6 @@ class ESP32Camera : public camera::Camera { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; /* public API (specific) */ void start_stream(camera::CameraRequester requester) override; void stop_stream(camera::CameraRequester requester) override; diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 0768b35507..7245ba7513 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -15,6 +15,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S2, VARIANT_ESP32S3, get_esp32_variant, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -121,6 +122,10 @@ def get_default_tx_enqueue_timeout(bit_rate): async def to_code(config): + # Legacy driver component provides driver/twai.h header + include_builtin_idf_component("driver") + # Also enable esp_driver_twai for future migration to new API + include_builtin_idf_component("esp_driver_twai") var = cg.new_Pvariable(config[CONF_ID]) await canbus.register_canbus(var, config) diff --git a/esphome/components/esp32_dac/output.py b/esphome/components/esp32_dac/output.py index daace596d3..7c63d7bd11 100644 --- a/esphome/components/esp32_dac/output.py +++ b/esphome/components/esp32_dac/output.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import output -from esphome.components.esp32 import VARIANT_ESP32, VARIANT_ESP32S2, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32, + VARIANT_ESP32S2, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_NUMBER, CONF_PIN @@ -38,6 +43,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( async def to_code(config): + include_builtin_idf_component("esp_driver_dac") var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await output.register_output(var, config) diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index ebcdd5f36e..a7d5f7e3d5 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -34,14 +34,29 @@ static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_M ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1); #ifdef USE_ESP32_HOSTED_HTTP_UPDATE +// Parse an integer from str, advancing ptr past the number +// Returns false if no digits were parsed +static bool parse_int(const char *&ptr, int &value) { + char *end; + value = static_cast(strtol(ptr, &end, 10)); + if (end == ptr) + return false; + ptr = end; + return true; +} + // Parse version string "major.minor.patch" into components -// Returns true if parsing succeeded +// Returns true if at least major.minor was parsed static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) { major = minor = patch = 0; - if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) { - return true; - } - return false; + const char *ptr = version_str.c_str(); + + if (!parse_int(ptr, major) || *ptr++ != '.' || !parse_int(ptr, minor)) + return false; + if (*ptr == '.') + parse_int(++ptr, patch); + + return true; } // Compare two versions, returns: @@ -196,11 +211,14 @@ bool Esp32HostedUpdate::fetch_manifest_() { int read_or_error = container->read(buf, sizeof(buf)); App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. if (result != http_request::HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT json_str.append(reinterpret_cast(buf), read_or_error); } container->end(); @@ -321,9 +339,14 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { App.feed_wdt(); yield(); - auto result = http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_request::http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == http_request::HttpReadLoopResult::RETRY) continue; + // Note: COMPLETE is currently unreachable since the loop condition checks bytes_read < content_length, + // but this is defensive code in case chunked transfer encoding support is added in the future. + if (result == http_request::HttpReadLoopResult::COMPLETE) + break; if (result != http_request::HttpReadLoopResult::DATA) { if (result == http_request::HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading firmware data"); diff --git a/esphome/components/esp32_rmt_led_strip/light.py b/esphome/components/esp32_rmt_led_strip/light.py index 3be3c758f1..6d41f6b5b8 100644 --- a/esphome/components/esp32_rmt_led_strip/light.py +++ b/esphome/components/esp32_rmt_led_strip/light.py @@ -5,6 +5,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import esp32, light from esphome.components.const import CONF_USE_PSRAM +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHIPSET, @@ -129,6 +130,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) await light.register_light(var, config) await cg.register_component(var, config) diff --git a/esphome/components/esp32_touch/__init__.py b/esphome/components/esp32_touch/__init__.py index c54ed8b9ea..a02370a343 100644 --- a/esphome/components/esp32_touch/__init__.py +++ b/esphome/components/esp32_touch/__init__.py @@ -6,6 +6,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, get_esp32_variant, gpio, + include_builtin_idf_component, ) import esphome.config_validation as cv from esphome.const import ( @@ -266,6 +267,11 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): + # Re-enable ESP-IDF's touch sensor driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_touch_sens") + # Legacy driver component provides driver/touch_sensor.h header + include_builtin_idf_component("driver") + touch = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(touch, config) diff --git a/esphome/components/esp32_touch/esp32_touch.h b/esphome/components/esp32_touch/esp32_touch.h index 812c746301..7f45f2ccb4 100644 --- a/esphome/components/esp32_touch/esp32_touch.h +++ b/esphome/components/esp32_touch/esp32_touch.h @@ -51,7 +51,6 @@ class ESP32TouchComponent : public Component { void setup() override; void dump_config() override; void loop() override; - float get_setup_priority() const override { return setup_priority::DATA; } void on_shutdown() override; diff --git a/esphome/components/esp8266/preferences.cpp b/esphome/components/esp8266/preferences.cpp index 35d1cd07f7..f037b881a8 100644 --- a/esphome/components/esp8266/preferences.cpp +++ b/esphome/components/esp8266/preferences.cpp @@ -17,10 +17,6 @@ namespace esphome::esp8266 { static const char *const TAG = "esp8266.preferences"; -static uint32_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128; static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4; @@ -43,6 +39,11 @@ static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 128; static constexpr uint32_t ESP8266_FLASH_STORAGE_SIZE = 64; #endif +static uint32_t + s_flash_storage[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) { if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) { return false; @@ -180,7 +181,6 @@ class ESP8266Preferences : public ESPPreferences { uint32_t current_flash_offset = 0; // in words void setup() { - s_flash_storage = new uint32_t[ESP8266_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); { @@ -283,10 +283,11 @@ class ESP8266Preferences : public ESPPreferences { } }; +static ESP8266Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new ESP8266Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - pref->setup(); - global_preferences = pref; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/espnow/__init__.py b/esphome/components/espnow/__init__.py index 1f5ca1104a..faeccd910e 100644 --- a/esphome/components/espnow/__init__.py +++ b/esphome/components/espnow/__init__.py @@ -13,7 +13,7 @@ from esphome.const import ( CONF_TRIGGER_ID, CONF_WIFI, ) -from esphome.core import CORE, HexInt +from esphome.core import HexInt from esphome.types import ConfigType CODEOWNERS = ["@jesserockz"] @@ -124,9 +124,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CORE.using_arduino: - cg.add_library("WiFi", None) - # ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks # This enables low-latency event processing instead of waiting for select() timeout socket.require_wake_loop_threadsafe() diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 1f2fe61fe1..38489ceb2b 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -1,6 +1,6 @@ import logging -from esphome import pins +from esphome import automation, pins import esphome.codegen as cg from esphome.components.esp32 import ( VARIANT_ESP32, @@ -14,6 +14,7 @@ from esphome.components.esp32 import ( add_idf_component, add_idf_sdkconfig_option, get_esp32_variant, + include_builtin_idf_component, ) from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -34,6 +35,8 @@ from esphome.const import ( CONF_MODE, CONF_MOSI_PIN, CONF_NUMBER, + CONF_ON_CONNECT, + CONF_ON_DISCONNECT, CONF_PAGE_ID, CONF_PIN, CONF_POLLING_INTERVAL, @@ -236,6 +239,8 @@ BASE_SCHEMA = cv.Schema( cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name, cv.Optional(CONF_USE_ADDRESS): cv.string_strict, cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True), + cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True), } ).extend(cv.COMPONENT_SCHEMA) @@ -419,12 +424,24 @@ async def to_code(config): # Also disable WiFi/BT coexistence since WiFi is disabled add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) + include_builtin_idf_component("esp_eth") + if config[CONF_TYPE] == "LAN8670": # Add LAN867x 10BASE-T1S PHY support component add_idf_component(name="espressif/lan867x", ref="2.0.0") - if CORE.using_arduino: - cg.add_library("WiFi", None) + if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_ETHERNET_CONNECT_TRIGGER") + await automation.build_automation( + var.get_connect_trigger(), [], on_connect_config + ) + + if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER") + await automation.build_automation( + var.get_disconnect_trigger(), [], on_disconnect_config + ) CORE.add_job(final_step) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 70f8ce1204..af7fed608b 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -309,6 +309,9 @@ void EthernetComponent::loop() { this->dump_connect_params_(); this->status_clear_warning(); +#ifdef USE_ETHERNET_CONNECT_TRIGGER + this->connect_trigger_.trigger(); +#endif } else if (now - this->connect_begin_ > 15000) { ESP_LOGW(TAG, "Connecting failed; reconnecting"); this->start_connect_(); @@ -318,10 +321,16 @@ void EthernetComponent::loop() { if (!this->started_) { ESP_LOGI(TAG, "Stopped connection"); this->state_ = EthernetComponentState::STOPPED; +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif } else if (!this->connected_) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = EthernetComponentState::CONNECTING; this->start_connect_(); +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + this->disconnect_trigger_.trigger(); +#endif } else { this->finish_connect_(); // When connected and stable, disable the loop to save CPU cycles diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 34380047d1..5a2869c5a7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -4,6 +4,7 @@ #include "esphome/core/defines.h" #include "esphome/core/hal.h" #include "esphome/core/helpers.h" +#include "esphome/core/automation.h" #include "esphome/components/network/ip_address.h" #ifdef USE_ESP32 @@ -119,6 +120,12 @@ class EthernetComponent : public Component { void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); } #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif protected: static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data); @@ -190,6 +197,12 @@ class EthernetComponent : public Component { StaticVector ip_state_listeners_; #endif +#ifdef USE_ETHERNET_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_ETHERNET_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index a983babe1c..d70a2940bc 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -2,21 +2,18 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace fan { static const char *const TAG = "fan"; +// Fan direction strings indexed by FanDirection enum (0-1): FORWARD, REVERSE, plus UNKNOWN +PROGMEM_STRING_TABLE(FanDirectionStrings, "FORWARD", "REVERSE", "UNKNOWN"); + const LogString *fan_direction_to_string(FanDirection direction) { - switch (direction) { - case FanDirection::FORWARD: - return LOG_STR("FORWARD"); - case FanDirection::REVERSE: - return LOG_STR("REVERSE"); - default: - return LOG_STR("UNKNOWN"); - } + return FanDirectionStrings::get_log_str(static_cast(direction), FanDirectionStrings::LAST_INDEX); } FanCall &FanCall::set_preset_mode(const std::string &preset_mode) { diff --git a/esphome/components/feedback/feedback_cover.cpp b/esphome/components/feedback/feedback_cover.cpp index e419ee6229..ffb19fa091 100644 --- a/esphome/components/feedback/feedback_cover.cpp +++ b/esphome/components/feedback/feedback_cover.cpp @@ -335,18 +335,18 @@ void FeedbackCover::start_direction_(CoverOperation dir) { switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->open_obstacle_; #endif break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; #ifdef USE_BINARY_SENSOR obstacle = this->close_obstacle_; #endif diff --git a/esphome/components/feedback/feedback_cover.h b/esphome/components/feedback/feedback_cover.h index 199d3b520a..6be8939413 100644 --- a/esphome/components/feedback/feedback_cover.h +++ b/esphome/components/feedback/feedback_cover.h @@ -17,9 +17,9 @@ class FeedbackCover : public cover::Cover, public Component { void loop() override; void dump_config() override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } #ifdef USE_BINARY_SENSOR void set_open_endstop(binary_sensor::BinarySensor *open_endstop); @@ -61,9 +61,9 @@ class FeedbackCover : public cover::Cover, public Component { binary_sensor::BinarySensor *close_obstacle_{nullptr}; #endif - Trigger<> *open_trigger_{new Trigger<>()}; - Trigger<> *close_trigger_{new Trigger<>()}; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; + Trigger<> close_trigger_; + Trigger<> stop_trigger_; uint32_t open_duration_{0}; uint32_t close_duration_{0}; diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index ddf38f2f55..8b381564b2 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -77,8 +77,6 @@ void GDK101Component::dump_config() { #endif // USE_TEXT_SENSOR } -float GDK101Component::get_setup_priority() const { return setup_priority::DATA; } - bool GDK101Component::read_bytes_with_retry_(uint8_t a_register, uint8_t *data, uint8_t len) { uint8_t retry = NUMBER_OF_READ_RETRIES; bool status = false; diff --git a/esphome/components/gdk101/gdk101.h b/esphome/components/gdk101/gdk101.h index f250a42a54..abe417e0f9 100644 --- a/esphome/components/gdk101/gdk101.h +++ b/esphome/components/gdk101/gdk101.h @@ -40,7 +40,6 @@ class GDK101Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/gp8403/gp8403.h b/esphome/components/gp8403/gp8403.h index 6613187b20..972f2ce60c 100644 --- a/esphome/components/gp8403/gp8403.h +++ b/esphome/components/gp8403/gp8403.h @@ -20,7 +20,6 @@ class GP8403Component : public Component, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } void set_model(GP8403Model model) { this->model_ = model; } void set_voltage(gp8403::GP8403Voltage voltage) { this->voltage_ = voltage; } diff --git a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp index 7a35596194..38ebbc90e4 100644 --- a/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp +++ b/esphome/components/gpio/binary_sensor/gpio_binary_sensor.cpp @@ -1,5 +1,6 @@ #include "gpio_binary_sensor.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace gpio { @@ -7,17 +8,12 @@ namespace gpio { static const char *const TAG = "gpio.binary_sensor"; #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG +// Interrupt type strings indexed by edge-triggered InterruptType values: +// indices 1-3: RISING_EDGE, FALLING_EDGE, ANY_EDGE; other values (e.g. level-triggered) map to UNKNOWN (index 0). +PROGMEM_STRING_TABLE(InterruptTypeStrings, "UNKNOWN", "RISING_EDGE", "FALLING_EDGE", "ANY_EDGE"); + static const LogString *interrupt_type_to_string(gpio::InterruptType type) { - switch (type) { - case gpio::INTERRUPT_RISING_EDGE: - return LOG_STR("RISING_EDGE"); - case gpio::INTERRUPT_FALLING_EDGE: - return LOG_STR("FALLING_EDGE"); - case gpio::INTERRUPT_ANY_EDGE: - return LOG_STR("ANY_EDGE"); - default: - return LOG_STR("UNKNOWN"); - } + return InterruptTypeStrings::get_log_str(static_cast(type), 0); } static const LogString *gpio_mode_to_string(bool use_interrupt) { diff --git a/esphome/components/hc8/hc8.cpp b/esphome/components/hc8/hc8.cpp index 4d0f77df1b..4c2d367b24 100644 --- a/esphome/components/hc8/hc8.cpp +++ b/esphome/components/hc8/hc8.cpp @@ -86,8 +86,6 @@ void HC8Component::calibrate(uint16_t baseline) { this->flush(); } -float HC8Component::get_setup_priority() const { return setup_priority::DATA; } - void HC8Component::dump_config() { ESP_LOGCONFIG(TAG, "HC8:\n" diff --git a/esphome/components/hc8/hc8.h b/esphome/components/hc8/hc8.h index 7711fb8c97..74257fab14 100644 --- a/esphome/components/hc8/hc8.h +++ b/esphome/components/hc8/hc8.h @@ -11,8 +11,6 @@ namespace esphome::hc8 { class HC8Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/hdc1080/hdc1080.h b/esphome/components/hdc1080/hdc1080.h index 7ad0764f1f..a5bece82c4 100644 --- a/esphome/components/hdc1080/hdc1080.h +++ b/esphome/components/hdc1080/hdc1080.h @@ -16,8 +16,6 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/hlw8012/hlw8012.cpp b/esphome/components/hlw8012/hlw8012.cpp index f037ee9d8b..d0fd697d8f 100644 --- a/esphome/components/hlw8012/hlw8012.cpp +++ b/esphome/components/hlw8012/hlw8012.cpp @@ -48,7 +48,6 @@ void HLW8012Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); LOG_SENSOR(" ", "Energy", this->energy_sensor_); } -float HLW8012Component::get_setup_priority() const { return setup_priority::DATA; } void HLW8012Component::update() { // HLW8012 has 50% duty cycle pulse_counter::pulse_counter_t raw_cf = this->cf_store_.read_raw_value(); diff --git a/esphome/components/hlw8012/hlw8012.h b/esphome/components/hlw8012/hlw8012.h index 312391f533..8a13ec07d8 100644 --- a/esphome/components/hlw8012/hlw8012.h +++ b/esphome/components/hlw8012/hlw8012.h @@ -31,7 +31,6 @@ class HLW8012Component : public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_initial_mode(HLW8012InitialMode initial_mode) { diff --git a/esphome/components/hlw8012/sensor.py b/esphome/components/hlw8012/sensor.py index 201ea4451f..4727877633 100644 --- a/esphome/components/hlw8012/sensor.py +++ b/esphome/components/hlw8012/sensor.py @@ -1,6 +1,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_CHANGE_MODE_EVERY, @@ -25,6 +26,7 @@ from esphome.const import ( UNIT_WATT, UNIT_WATT_HOURS, ) +from esphome.core import CORE AUTO_LOAD = ["pulse_counter"] @@ -91,6 +93,12 @@ CONFIG_SCHEMA = cv.Schema( async def to_code(config): + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # HLW8012 uses pulse_counter's PCNT storage which requires driver/pcnt.h + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/hm3301/hm3301.cpp b/esphome/components/hm3301/hm3301.cpp index 00fb85397c..9343b47823 100644 --- a/esphome/components/hm3301/hm3301.cpp +++ b/esphome/components/hm3301/hm3301.cpp @@ -31,8 +31,6 @@ void HM3301Component::dump_config() { LOG_SENSOR(" ", "AQI", this->aqi_sensor_); } -float HM3301Component::get_setup_priority() const { return setup_priority::DATA; } - void HM3301Component::update() { if (this->read(data_buffer_, 29) != i2c::ERROR_OK) { ESP_LOGW(TAG, "Read result failed"); diff --git a/esphome/components/hm3301/hm3301.h b/esphome/components/hm3301/hm3301.h index e155ed6b4b..6b10a5e237 100644 --- a/esphome/components/hm3301/hm3301.h +++ b/esphome/components/hm3301/hm3301.h @@ -23,7 +23,6 @@ class HM3301Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hmc5883l/hmc5883l.cpp b/esphome/components/hmc5883l/hmc5883l.cpp index 101493ad91..b62381a287 100644 --- a/esphome/components/hmc5883l/hmc5883l.cpp +++ b/esphome/components/hmc5883l/hmc5883l.cpp @@ -83,7 +83,6 @@ void HMC5883LComponent::dump_config() { LOG_SENSOR(" ", "Z Axis", this->z_sensor_); LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float HMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } void HMC5883LComponent::update() { uint16_t raw_x, raw_y, raw_z; if (!this->read_byte_16(HMC5883L_REGISTER_DATA_X_MSB, &raw_x) || diff --git a/esphome/components/hmc5883l/hmc5883l.h b/esphome/components/hmc5883l/hmc5883l.h index 06fba2af9d..8eae0f7a50 100644 --- a/esphome/components/hmc5883l/hmc5883l.h +++ b/esphome/components/hmc5883l/hmc5883l.h @@ -39,7 +39,6 @@ class HMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_oversampling(HMC5883LOversampling oversampling) { oversampling_ = oversampling; } diff --git a/esphome/components/homeassistant/time/homeassistant_time.cpp b/esphome/components/homeassistant/time/homeassistant_time.cpp index e72c5a21f5..d039892073 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.cpp +++ b/esphome/components/homeassistant/time/homeassistant_time.cpp @@ -11,8 +11,6 @@ void HomeassistantTime::dump_config() { RealTimeClock::dump_config(); } -float HomeassistantTime::get_setup_priority() const { return setup_priority::DATA; } - void HomeassistantTime::setup() { global_homeassistant_time = this; } void HomeassistantTime::update() { api::global_api_server->request_time(); } diff --git a/esphome/components/homeassistant/time/homeassistant_time.h b/esphome/components/homeassistant/time/homeassistant_time.h index 36e28ea16b..7b5842fefd 100644 --- a/esphome/components/homeassistant/time/homeassistant_time.h +++ b/esphome/components/homeassistant/time/homeassistant_time.h @@ -13,7 +13,6 @@ class HomeassistantTime : public time::RealTimeClock { void update() override; void dump_config() override; void set_epoch_time(uint32_t epoch) { this->synchronize_epoch_(epoch); } - float get_setup_priority() const override; }; extern HomeassistantTime *global_homeassistant_time; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp index 5f2b009972..904672d136 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.cpp @@ -91,7 +91,5 @@ void HoneywellHIComponent::dump_config() { LOG_UPDATE_INTERVAL(this); } -float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace honeywell_hih_i2c } // namespace esphome diff --git a/esphome/components/honeywell_hih_i2c/honeywell_hih.h b/esphome/components/honeywell_hih_i2c/honeywell_hih.h index 4457eab1da..79140f7399 100644 --- a/esphome/components/honeywell_hih_i2c/honeywell_hih.h +++ b/esphome/components/honeywell_hih_i2c/honeywell_hih.h @@ -11,7 +11,6 @@ namespace honeywell_hih_i2c { class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice { public: void dump_config() override; - float get_setup_priority() const override; void loop() override; void update() override; diff --git a/esphome/components/host/preferences.cpp b/esphome/components/host/preferences.cpp index 7b939cdebb..5ad87c1f2a 100644 --- a/esphome/components/host/preferences.cpp +++ b/esphome/components/host/preferences.cpp @@ -66,10 +66,11 @@ ESPPreferenceObject HostPreferences::make_preference(size_t length, uint32_t typ return ESPPreferenceObject(backend); }; +static HostPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *pref = new HostPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - host_preferences = pref; - global_preferences = pref; + host_preferences = &s_preferences; + global_preferences = &s_preferences; } bool HostPreferenceBackend::save(const uint8_t *data, size_t len) { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index cde6886109..972e72c170 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -43,7 +43,6 @@ void HTE501Component::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float HTE501Component::get_setup_priority() const { return setup_priority::DATA; } void HTE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/hte501/hte501.h b/esphome/components/hte501/hte501.h index a7072d5bdb..b47daf9157 100644 --- a/esphome/components/hte501/hte501.h +++ b/esphome/components/hte501/hte501.h @@ -13,7 +13,6 @@ class HTE501Component : public PollingComponent, public i2c::I2CDevice { void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 07bc758037..64d74323d6 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -155,6 +155,9 @@ async def to_code(config): cg.add(var.set_watchdog_timeout(timeout_ms)) if CORE.is_esp32: + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_http_client") + cg.add(var.set_buffer_size_rx(config[CONF_BUFFER_SIZE_RX])) cg.add(var.set_buffer_size_tx(config[CONF_BUFFER_SIZE_TX])) cg.add(var.set_verify_ssl(config[CONF_VERIFY_SSL])) diff --git a/esphome/components/http_request/http_request.h b/esphome/components/http_request/http_request.h index fb39ca504c..c88360ca78 100644 --- a/esphome/components/http_request/http_request.h +++ b/esphome/components/http_request/http_request.h @@ -26,6 +26,7 @@ struct Header { enum HttpStatus { HTTP_STATUS_OK = 200, HTTP_STATUS_NO_CONTENT = 204, + HTTP_STATUS_RESET_CONTENT = 205, HTTP_STATUS_PARTIAL_CONTENT = 206, /* 3xx - Redirection */ @@ -126,19 +127,21 @@ struct HttpReadResult { /// Result of processing a non-blocking read with timeout (for manual loops) enum class HttpReadLoopResult : uint8_t { - DATA, ///< Data was read, process it - RETRY, ///< No data yet, already delayed, caller should continue loop - ERROR, ///< Read error, caller should exit loop - TIMEOUT, ///< Timeout waiting for data, caller should exit loop + DATA, ///< Data was read, process it + COMPLETE, ///< All content has been read, caller should exit loop + RETRY, ///< No data yet, already delayed, caller should continue loop + ERROR, ///< Read error, caller should exit loop + TIMEOUT, ///< Timeout waiting for data, caller should exit loop }; /// Process a read result with timeout tracking and delay handling /// @param bytes_read_or_error Return value from read() - positive for bytes read, negative for error /// @param last_data_time Time of last successful read, updated when data received /// @param timeout_ms Maximum time to wait for data -/// @return DATA if data received, RETRY if should continue loop, ERROR/TIMEOUT if should exit -inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, - uint32_t timeout_ms) { +/// @param is_read_complete Whether all expected content has been read (from HttpContainer::is_read_complete()) +/// @return How the caller should proceed - see HttpReadLoopResult enum +inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_t &last_data_time, uint32_t timeout_ms, + bool is_read_complete) { if (bytes_read_or_error > 0) { last_data_time = millis(); return HttpReadLoopResult::DATA; @@ -146,7 +149,10 @@ inline HttpReadLoopResult http_read_loop_result(int bytes_read_or_error, uint32_ if (bytes_read_or_error < 0) { return HttpReadLoopResult::ERROR; } - // bytes_read_or_error == 0: no data available yet + // bytes_read_or_error == 0: either "no data yet" or "all content read" + if (is_read_complete) { + return HttpReadLoopResult::COMPLETE; + } if (millis() - last_data_time >= timeout_ms) { return HttpReadLoopResult::TIMEOUT; } @@ -159,9 +165,9 @@ class HttpRequestComponent; class HttpContainer : public Parented { public: virtual ~HttpContainer() = default; - size_t content_length; - int status_code; - uint32_t duration_ms; + size_t content_length{0}; + int status_code{-1}; ///< -1 indicates no response received yet + uint32_t duration_ms{0}; /** * @brief Read data from the HTTP response body. @@ -194,9 +200,24 @@ class HttpContainer : public Parented { virtual void end() = 0; void set_secure(bool secure) { this->secure_ = secure; } + void set_chunked(bool chunked) { this->is_chunked_ = chunked; } size_t get_bytes_read() const { return this->bytes_read_; } + /// Check if all expected content has been read + /// For chunked responses, returns false (completion detected via read() returning error/EOF) + bool is_read_complete() const { + // Per RFC 9112, these responses have no body: + // - 1xx (Informational), 204 No Content, 205 Reset Content, 304 Not Modified + if ((this->status_code >= 100 && this->status_code < 200) || this->status_code == HTTP_STATUS_NO_CONTENT || + this->status_code == HTTP_STATUS_RESET_CONTENT || this->status_code == HTTP_STATUS_NOT_MODIFIED) { + return true; + } + // For non-chunked responses, complete when bytes_read >= content_length + // This handles both Content-Length: 0 and Content-Length: N cases + return !this->is_chunked_ && this->bytes_read_ >= this->content_length; + } + /** * @brief Get response headers. * @@ -209,6 +230,7 @@ class HttpContainer : public Parented { protected: size_t bytes_read_{0}; bool secure_{false}; + bool is_chunked_{false}; ///< True if response uses chunked transfer encoding std::map> response_headers_{}; }; @@ -219,7 +241,7 @@ class HttpContainer : public Parented { /// @param total_size Total bytes to read /// @param chunk_size Maximum bytes per read call /// @param timeout_ms Read timeout in milliseconds -/// @return HttpReadResult with status and error_code on failure +/// @return HttpReadResult with status and error_code on failure; use container->get_bytes_read() for total bytes read inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, size_t total_size, size_t chunk_size, uint32_t timeout_ms) { size_t read_index = 0; @@ -231,9 +253,11 @@ inline HttpReadResult http_read_fully(HttpContainer *container, uint8_t *buffer, App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms); + auto result = http_read_loop_result(read_bytes_or_error, last_data_time, timeout_ms, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + if (result == HttpReadLoopResult::COMPLETE) + break; // Server sent less data than requested, but transfer is complete if (result == HttpReadLoopResult::ERROR) return {HttpReadStatus::ERROR, read_bytes_or_error}; if (result == HttpReadLoopResult::TIMEOUT) @@ -332,13 +356,13 @@ template class HttpRequestSendAction : public Action { void set_json(std::function json_func) { this->json_func_ = json_func; } #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *get_success_trigger_with_response() const { - return this->success_trigger_with_response_; + Trigger, std::string &, Ts...> *get_success_trigger_with_response() { + return &this->success_trigger_with_response_; } #endif - Trigger, Ts...> *get_success_trigger() const { return this->success_trigger_; } + Trigger, Ts...> *get_success_trigger() { return &this->success_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } + Trigger *get_error_trigger() { 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; @@ -372,7 +396,7 @@ template class HttpRequestSendAction : public Action { auto captured_args = std::make_tuple(x...); if (container == nullptr) { - std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, + std::apply([this](Ts... captured_args_inner) { this->error_trigger_.trigger(captured_args_inner...); }, captured_args); return; } @@ -393,11 +417,12 @@ template class HttpRequestSendAction : public Action { int read_or_error = container->read(buf + read_index, std::min(max_length - read_index, 512)); App.feed_wdt(); yield(); - auto result = http_read_loop_result(read_or_error, last_data_time, read_timeout); + auto result = + http_read_loop_result(read_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; if (result != HttpReadLoopResult::DATA) - break; // ERROR or TIMEOUT + break; // COMPLETE, ERROR, or TIMEOUT read_index += read_or_error; } response_body.reserve(read_index); @@ -406,14 +431,14 @@ template class HttpRequestSendAction : public Action { } std::apply( [this, &container, &response_body](Ts... captured_args_inner) { - this->success_trigger_with_response_->trigger(container, response_body, 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...); }, + Ts... captured_args_inner) { this->success_trigger_.trigger(container, captured_args_inner...); }, captured_args); } container->end(); @@ -433,12 +458,10 @@ template class HttpRequestSendAction : public Action { std::map> json_{}; std::function json_func_{nullptr}; #ifdef USE_HTTP_REQUEST_RESPONSE - Trigger, std::string &, Ts...> *success_trigger_with_response_ = - new Trigger, std::string &, Ts...>(); + Trigger, std::string &, Ts...> success_trigger_with_response_; #endif - Trigger, Ts...> *success_trigger_ = - new Trigger, Ts...>(); - Trigger *error_trigger_ = new Trigger(); + Trigger, Ts...> success_trigger_; + Trigger error_trigger_; size_t max_response_buffer_size_{SIZE_MAX}; }; diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 82538b2cb3..aee1f651bf 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -133,11 +133,15 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // HTTPClient::getSize() returns -1 for chunked transfer encoding (no Content-Length). // When cast to size_t, -1 becomes SIZE_MAX (4294967295 on 32-bit). - // The read() method handles this: bytes_read_ can never reach SIZE_MAX, so the - // early return check (bytes_read_ >= content_length) will never trigger. + // The read() method uses a chunked transfer encoding decoder (read_chunked_) to strip + // chunk framing and deliver only decoded content. When the final 0-size chunk is received, + // is_chunked_ is cleared and content_length is set to the actual decoded size, so + // is_read_complete() returns true and callers exit their read loops correctly. int content_length = container->client_.getSize(); ESP_LOGD(TAG, "Content-Length: %d", content_length); container->content_length = (size_t) content_length; + // -1 (SIZE_MAX when cast to size_t) means chunked transfer encoding + container->set_chunked(content_length == -1); container->duration_ms = millis() - start; return container; @@ -160,6 +164,10 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur // > 0: bytes read // 0: no data yet, retry <-- NOTE: 0 means retry, NOT EOF! // < 0: error/connection closed <-- connection closed returns -1, not 0 +// +// For chunked transfer encoding, read_chunked_() decodes chunk framing and delivers +// only the payload data. When the final 0-size chunk is received, it clears is_chunked_ +// and sets content_length = bytes_read_ so is_read_complete() returns true. int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); @@ -170,24 +178,42 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return HTTP_ERROR_CONNECTION_CLOSED; } + if (this->is_chunked_) { + int result = this->read_chunked_(buf, max_len, stream_ptr); + this->duration_ms += (millis() - start); + if (result > 0) { + return result; + } + // result <= 0: check for completion or errors + if (this->is_read_complete()) { + return 0; // Chunked transfer complete (final 0-size chunk received) + } + if (result < 0) { + return result; // Stream error during chunk decoding + } + // read_chunked_ returned 0: no data was available (available() was 0). + // This happens when the TCP buffer is empty - either more data is in flight, + // or the connection dropped. Arduino's connected() returns false only when + // both the remote has closed AND the receive buffer is empty, so any buffered + // data is fully drained before we report the drop. + if (!stream_ptr->connected()) { + return HTTP_ERROR_CONNECTION_CLOSED; + } + return 0; // No data yet, caller should retry + } + + // Non-chunked path int available_data = stream_ptr->available(); - // For chunked transfer encoding, HTTPClient::getSize() returns -1, which becomes SIZE_MAX when - // cast to size_t. SIZE_MAX - bytes_read_ is still huge, so it won't limit the read. size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len; int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data)); if (bufsize == 0) { this->duration_ms += (millis() - start); - // Check if we've read all expected content (only valid when content_length is known and not SIZE_MAX) - // For chunked encoding (content_length == SIZE_MAX), we can't use this check - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + if (this->is_read_complete()) { return 0; // All content read successfully } - // No data available - check if connection is still open - // For chunked encoding, !connected() after reading means EOF (all chunks received) - // For known content_length with bytes_read_ < content_length, it means connection dropped if (!stream_ptr->connected()) { - return HTTP_ERROR_CONNECTION_CLOSED; // Connection closed or EOF for chunked + return HTTP_ERROR_CONNECTION_CLOSED; } return 0; // No data yet, caller should retry } @@ -201,6 +227,143 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) { return read_len; } +void HttpContainerArduino::chunk_header_complete_() { + if (this->chunk_remaining_ == 0) { + this->chunk_state_ = ChunkedState::CHUNK_TRAILER; + this->chunk_remaining_ = 1; // repurpose as at-start-of-line flag + } else { + this->chunk_state_ = ChunkedState::CHUNK_DATA; + } +} + +// Chunked transfer encoding decoder +// +// On Arduino, getStreamPtr() returns raw TCP data. For chunked responses, this includes +// chunk framing (size headers, CRLF delimiters) mixed with payload data. This decoder +// strips the framing and delivers only decoded content to the caller. +// +// Chunk format (RFC 9112 Section 7.1): +// [;extension]\r\n +// \r\n +// ... +// 0\r\n +// [trailer-field\r\n]* +// \r\n +// +// Non-blocking: only processes bytes already in the TCP receive buffer. +// State (chunk_state_, chunk_remaining_) is preserved between calls, so partial +// chunk headers or split \r\n sequences resume correctly on the next call. +// Framing bytes (hex sizes, \r\n) may be consumed without producing output; +// the caller sees 0 and retries via the normal read timeout logic. +// +// WiFiClient::read() returns -1 on error despite available() > 0 (connection reset +// between check and read). On any stream error (c < 0 or readBytes <= 0), we return +// already-decoded data if any; otherwise HTTP_ERROR_CONNECTION_CLOSED. The error +// will surface again on the next call since the stream stays broken. +// +// Returns: > 0 decoded bytes, 0 no data available, < 0 error +int HttpContainerArduino::read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream) { + int total_decoded = 0; + + while (total_decoded < (int) max_len && this->chunk_state_ != ChunkedState::COMPLETE) { + // Non-blocking: only process what's already buffered + if (stream->available() == 0) + break; + + // CHUNK_DATA reads multiple bytes; handle before the single-byte switch + if (this->chunk_state_ == ChunkedState::CHUNK_DATA) { + // Only read what's available, what fits in buf, and what remains in this chunk + size_t to_read = + std::min({max_len - (size_t) total_decoded, this->chunk_remaining_, (size_t) stream->available()}); + if (to_read == 0) + break; + App.feed_wdt(); + int read_len = stream->readBytes(buf + total_decoded, to_read); + if (read_len <= 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + total_decoded += read_len; + this->chunk_remaining_ -= read_len; + this->bytes_read_ += read_len; + if (this->chunk_remaining_ == 0) + this->chunk_state_ = ChunkedState::CHUNK_DATA_TRAIL; + continue; + } + + // All other states consume a single byte + int c = stream->read(); + if (c < 0) + return total_decoded > 0 ? total_decoded : HTTP_ERROR_CONNECTION_CLOSED; + + switch (this->chunk_state_) { + // Parse hex chunk size, one byte at a time: "[;ext]\r\n" + // Note: if no hex digits are parsed (e.g., bare \r\n), chunk_remaining_ stays 0 + // and is treated as the final chunk. This is intentionally lenient — on embedded + // devices, rejecting malformed framing is less useful than terminating cleanly. + // Overflow of chunk_remaining_ from extremely long hex strings (>8 digits on + // 32-bit) is not checked; >4GB chunks are unrealistic on embedded targets and + // would simply cause fewer bytes to be read from that chunk. + case ChunkedState::CHUNK_HEADER: + if (c == '\n') { + // \n terminates the size line; chunk_remaining_ == 0 means last chunk + this->chunk_header_complete_(); + } else { + uint8_t hex = parse_hex_char(c); + if (hex != INVALID_HEX_CHAR) { + this->chunk_remaining_ = (this->chunk_remaining_ << 4) | hex; + } else if (c != '\r') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER_EXT; // ';' starts extension, skip to \n + } + } + break; + + // Skip chunk extension bytes until \n (e.g., ";name=value\r\n") + case ChunkedState::CHUNK_HEADER_EXT: + if (c == '\n') { + this->chunk_header_complete_(); + } + break; + + // Consume \r\n trailing each chunk's data + case ChunkedState::CHUNK_DATA_TRAIL: + if (c == '\n') { + this->chunk_state_ = ChunkedState::CHUNK_HEADER; + this->chunk_remaining_ = 0; // reset for next chunk's hex accumulation + } + // else: \r is consumed silently, next iteration gets \n + break; + + // Consume optional trailer headers and terminating empty line after final chunk. + // Per RFC 9112 Section 7.1: "0\r\n" is followed by optional "field\r\n" lines + // and a final "\r\n". chunk_remaining_ is repurposed as a flag: 1 = at start + // of line (may be the empty terminator), 0 = mid-line (reading a trailer field). + case ChunkedState::CHUNK_TRAILER: + if (c == '\n') { + if (this->chunk_remaining_ != 0) { + this->chunk_state_ = ChunkedState::COMPLETE; // Empty line terminates trailers + } else { + this->chunk_remaining_ = 1; // End of trailer field, at start of next line + } + } else if (c != '\r') { + this->chunk_remaining_ = 0; // Non-CRLF char: reading a trailer field + } + // \r doesn't change the flag — it's part of \r\n line endings + break; + + default: + break; + } + + if (this->chunk_state_ == ChunkedState::COMPLETE) { + // Clear chunked flag and set content_length to actual decoded size so + // is_read_complete() returns true and callers exit their read loops + this->is_chunked_ = false; + this->content_length = this->bytes_read_; + } + } + + return total_decoded; +} + void HttpContainerArduino::end() { watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); this->client_.end(); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d9b5af9d81..a1084b12d5 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -18,6 +18,17 @@ namespace esphome::http_request { class HttpRequestArduino; + +/// State machine for decoding chunked transfer encoding on Arduino +enum class ChunkedState : uint8_t { + CHUNK_HEADER, ///< Reading hex digits of chunk size + CHUNK_HEADER_EXT, ///< Skipping chunk extensions until \n + CHUNK_DATA, ///< Reading chunk data bytes + CHUNK_DATA_TRAIL, ///< Skipping \r\n after chunk data + CHUNK_TRAILER, ///< Consuming trailer headers after final 0-size chunk + COMPLETE, ///< Finished: final chunk and trailers consumed +}; + class HttpContainerArduino : public HttpContainer { public: int read(uint8_t *buf, size_t max_len) override; @@ -26,6 +37,13 @@ class HttpContainerArduino : public HttpContainer { protected: friend class HttpRequestArduino; HTTPClient client_{}; + + /// Decode chunked transfer encoding from the raw stream + int read_chunked_(uint8_t *buf, size_t max_len, WiFiClient *stream); + /// Transition from chunk header to data or trailer based on parsed size + void chunk_header_complete_(); + ChunkedState chunk_state_{ChunkedState::CHUNK_HEADER}; + size_t chunk_remaining_{0}; ///< Bytes remaining in current chunk }; class HttpRequestArduino : public HttpRequestComponent { diff --git a/esphome/components/http_request/http_request_idf.cpp b/esphome/components/http_request/http_request_idf.cpp index 2b4dee953a..bd12b7d123 100644 --- a/esphome/components/http_request/http_request_idf.cpp +++ b/esphome/components/http_request/http_request_idf.cpp @@ -160,6 +160,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c // esp_http_client_fetch_headers() returns 0 for chunked transfer encoding (no Content-Length header). // The read() method handles content_length == 0 specially to support chunked responses. container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -195,6 +196,7 @@ std::shared_ptr HttpRequestIDF::perform(const std::string &url, c container->feed_wdt(); container->content_length = esp_http_client_fetch_headers(client); + container->set_chunked(esp_http_client_is_chunked_response(client)); container->feed_wdt(); container->status_code = esp_http_client_get_status_code(client); container->feed_wdt(); @@ -239,10 +241,9 @@ int HttpContainerIDF::read(uint8_t *buf, size_t max_len) { const uint32_t start = millis(); watchdog::WatchdogManager wdm(this->parent_->get_watchdog_timeout()); - // Check if we've already read all expected content - // Skip this check when content_length is 0 (chunked transfer encoding or unknown length) - // For chunked responses, esp_http_client_read() will return 0 when all data is received - if (this->content_length > 0 && this->bytes_read_ >= this->content_length) { + // Check if we've already read all expected content (non-chunked only) + // For chunked responses (content_length == 0), esp_http_client_read() handles EOF + if (this->is_read_complete()) { return 0; // All content read successfully } diff --git a/esphome/components/http_request/ota/ota_http_request.cpp b/esphome/components/http_request/ota/ota_http_request.cpp index 8a4b3684cf..882def4d7f 100644 --- a/esphome/components/http_request/ota/ota_http_request.cpp +++ b/esphome/components/http_request/ota/ota_http_request.cpp @@ -130,9 +130,15 @@ uint8_t OtaHttpRequestComponent::do_ota_() { App.feed_wdt(); yield(); - auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout); + auto result = http_read_loop_result(bufsize_or_error, last_data_time, read_timeout, container->is_read_complete()); if (result == HttpReadLoopResult::RETRY) continue; + // For non-chunked responses, COMPLETE is unreachable (loop condition checks bytes_read < content_length). + // For chunked responses, the decoder sets content_length = bytes_read when the final chunk arrives, + // which causes the loop condition to terminate. But COMPLETE can still be returned if the decoder + // finishes mid-read, so this is needed for correctness. + if (result == HttpReadLoopResult::COMPLETE) + break; if (result != HttpReadLoopResult::DATA) { if (result == HttpReadLoopResult::TIMEOUT) { ESP_LOGE(TAG, "Timeout reading data"); diff --git a/esphome/components/htu21d/htu21d.cpp b/esphome/components/htu21d/htu21d.cpp index c5d91d3dd0..58a28b213f 100644 --- a/esphome/components/htu21d/htu21d.cpp +++ b/esphome/components/htu21d/htu21d.cpp @@ -143,7 +143,5 @@ uint8_t HTU21DComponent::get_heater_level() { return raw_heater & 0xF; } -float HTU21DComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace htu21d } // namespace esphome diff --git a/esphome/components/htu21d/htu21d.h b/esphome/components/htu21d/htu21d.h index 277c6ca3e5..594be78326 100644 --- a/esphome/components/htu21d/htu21d.h +++ b/esphome/components/htu21d/htu21d.h @@ -28,8 +28,6 @@ class HTU21DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_level(uint8_t level); uint8_t get_heater_level(); - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/htu31d/htu31d.cpp b/esphome/components/htu31d/htu31d.cpp index 562078aacb..4bb38a11a2 100644 --- a/esphome/components/htu31d/htu31d.cpp +++ b/esphome/components/htu31d/htu31d.cpp @@ -259,11 +259,5 @@ void HTU31DComponent::set_heater_state(bool desired) { } } -/** - * Sets the startup priority for this component. - * - * @returns The startup priority - */ -float HTU31DComponent::get_setup_priority() const { return setup_priority::DATA; } } // namespace htu31d } // namespace esphome diff --git a/esphome/components/htu31d/htu31d.h b/esphome/components/htu31d/htu31d.h index 9462133ced..24d85243cc 100644 --- a/esphome/components/htu31d/htu31d.h +++ b/esphome/components/htu31d/htu31d.h @@ -20,8 +20,6 @@ class HTU31DComponent : public PollingComponent, public i2c::I2CDevice { void set_heater_state(bool desired); bool is_heater_enabled(); - float get_setup_priority() const override; - protected: bool reset_(); uint32_t read_serial_num_(); diff --git a/esphome/components/hx711/hx711.cpp b/esphome/components/hx711/hx711.cpp index 67ec4549df..f2e3234127 100644 --- a/esphome/components/hx711/hx711.cpp +++ b/esphome/components/hx711/hx711.cpp @@ -22,7 +22,6 @@ void HX711Sensor::dump_config() { LOG_PIN(" SCK Pin: ", this->sck_pin_); LOG_UPDATE_INTERVAL(this); } -float HX711Sensor::get_setup_priority() const { return setup_priority::DATA; } void HX711Sensor::update() { uint32_t result; if (this->read_sensor_(&result)) { diff --git a/esphome/components/hx711/hx711.h b/esphome/components/hx711/hx711.h index a92bb9945d..37723ee81f 100644 --- a/esphome/components/hx711/hx711.h +++ b/esphome/components/hx711/hx711.h @@ -23,7 +23,6 @@ class HX711Sensor : public sensor::Sensor, public PollingComponent { void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp index 4872d68610..983a0a6649 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.cpp @@ -284,7 +284,5 @@ void HydreonRGxxComponent::process_line_() { } } -float HydreonRGxxComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hydreon_rgxx } // namespace esphome diff --git a/esphome/components/hydreon_rgxx/hydreon_rgxx.h b/esphome/components/hydreon_rgxx/hydreon_rgxx.h index 76b0985a24..e3f9798a93 100644 --- a/esphome/components/hydreon_rgxx/hydreon_rgxx.h +++ b/esphome/components/hydreon_rgxx/hydreon_rgxx.h @@ -53,8 +53,6 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void set_disable_led(bool disable_led) { this->disable_led_ = disable_led; } protected: diff --git a/esphome/components/hyt271/hyt271.cpp b/esphome/components/hyt271/hyt271.cpp index f187e054a8..4c0e3cd96e 100644 --- a/esphome/components/hyt271/hyt271.cpp +++ b/esphome/components/hyt271/hyt271.cpp @@ -46,7 +46,5 @@ void HYT271Component::update() { this->status_clear_warning(); }); } -float HYT271Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace hyt271 } // namespace esphome diff --git a/esphome/components/hyt271/hyt271.h b/esphome/components/hyt271/hyt271.h index 64f32a651c..19409d830c 100644 --- a/esphome/components/hyt271/hyt271.h +++ b/esphome/components/hyt271/hyt271.h @@ -16,8 +16,6 @@ class HYT271Component : public PollingComponent, public i2c::I2CDevice { /// Update the sensor values (temperature+humidity). void update() override; - float get_setup_priority() const override; - protected: sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/esphome/components/i2c/__init__.py b/esphome/components/i2c/__init__.py index 19efda0b49..de3f2be674 100644 --- a/esphome/components/i2c/__init__.py +++ b/esphome/components/i2c/__init__.py @@ -183,7 +183,7 @@ async def to_code(config): if CORE.using_zephyr: zephyr_add_prj_conf("I2C", True) i2c = "i2c0" - if zephyr_data()[KEY_BOARD] in ["xiao_ble"]: + if zephyr_data()[KEY_BOARD] == "xiao_ble": i2c = "i2c1" zephyr_add_overlay( f""" diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index c1e7336ce4..b9b5d79428 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -11,12 +11,6 @@ namespace i2c { static const char *const TAG = "i2c"; void I2CBus::i2c_scan_() { - // suppress logs from the IDF I2C library during the scan -#if defined(USE_ESP32) && defined(USE_LOGGER) - auto previous = esp_log_level_get("*"); - esp_log_level_set("*", ESP_LOG_NONE); -#endif - for (uint8_t address = 8; address != 120; address++) { auto err = write_readv(address, nullptr, 0, nullptr, 0); if (err == ERROR_OK) { @@ -27,9 +21,6 @@ void I2CBus::i2c_scan_() { // it takes 16sec to scan on nrf52. It prevents board reset. arch_feed_wdt(); } -#if defined(USE_ESP32) && defined(USE_LOGGER) - esp_log_level_set("*", previous); -#endif } ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len) { diff --git a/esphome/components/i2s_audio/__init__.py b/esphome/components/i2s_audio/__init__.py index d3128c5f4c..1cd2e97a5e 100644 --- a/esphome/components/i2s_audio/__init__.py +++ b/esphome/components/i2s_audio/__init__.py @@ -1,6 +1,11 @@ from esphome import pins import esphome.codegen as cg from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + get_esp32_variant, + include_builtin_idf_component, +) +from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32C5, @@ -10,8 +15,6 @@ from esphome.components.esp32 import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, - add_idf_sdkconfig_option, - get_esp32_variant, ) import esphome.config_validation as cv from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE @@ -272,6 +275,10 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + + # Re-enable ESP-IDF's I2S driver (excluded by default to save compile time) + include_builtin_idf_component("esp_driver_i2s") + if use_legacy(): cg.add_define("USE_I2S_LEGACY") diff --git a/esphome/components/i2s_audio/media_player/__init__.py b/esphome/components/i2s_audio/media_player/__init__.py index 35c42e1b06..426b211f47 100644 --- a/esphome/components/i2s_audio/media_player/__init__.py +++ b/esphome/components/i2s_audio/media_player/__init__.py @@ -114,6 +114,7 @@ async def to_code(config): cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) + cg.add_library("WiFi", None) cg.add_library("NetworkClientSecure", None) cg.add_library("HTTPClient", None) cg.add_library("esphome/ESP32-audioI2S", "2.3.0") diff --git a/esphome/components/ina219/ina219.cpp b/esphome/components/ina219/ina219.cpp index ea8c5cea9d..278017651b 100644 --- a/esphome/components/ina219/ina219.cpp +++ b/esphome/components/ina219/ina219.cpp @@ -151,8 +151,6 @@ void INA219Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA219Component::get_setup_priority() const { return setup_priority::DATA; } - void INA219Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina219/ina219.h b/esphome/components/ina219/ina219.h index 115fa886e0..bcadb65e36 100644 --- a/esphome/components/ina219/ina219.h +++ b/esphome/components/ina219/ina219.h @@ -13,7 +13,6 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void on_powerdown() override; diff --git a/esphome/components/ina226/ina226.cpp b/esphome/components/ina226/ina226.cpp index c4d4fb896e..cbc44c9a1a 100644 --- a/esphome/components/ina226/ina226.cpp +++ b/esphome/components/ina226/ina226.cpp @@ -104,8 +104,6 @@ void INA226Component::dump_config() { LOG_SENSOR(" ", "Power", this->power_sensor_); } -float INA226Component::get_setup_priority() const { return setup_priority::DATA; } - void INA226Component::update() { if (this->bus_voltage_sensor_ != nullptr) { uint16_t raw_bus_voltage; diff --git a/esphome/components/ina226/ina226.h b/esphome/components/ina226/ina226.h index 61214fea0e..0aa66ff765 100644 --- a/esphome/components/ina226/ina226.h +++ b/esphome/components/ina226/ina226.h @@ -45,7 +45,6 @@ class INA226Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } diff --git a/esphome/components/ina2xx_base/__init__.py b/esphome/components/ina2xx_base/__init__.py index ce68ad2726..15e2faba07 100644 --- a/esphome/components/ina2xx_base/__init__.py +++ b/esphome/components/ina2xx_base/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import UNIT_AMPERE_HOUR import esphome.config_validation as cv from esphome.const import ( CONF_BUS_VOLTAGE, @@ -36,7 +37,6 @@ CONF_CHARGE_COULOMBS = "charge_coulombs" CONF_ENERGY_JOULES = "energy_joules" CONF_TEMPERATURE_COEFFICIENT = "temperature_coefficient" CONF_RESET_ON_BOOT = "reset_on_boot" -UNIT_AMPERE_HOURS = "Ah" UNIT_COULOMB = "C" UNIT_JOULE = "J" UNIT_MILLIVOLT = "mV" @@ -180,7 +180,7 @@ INA2XX_SCHEMA = cv.Schema( ), cv.Optional(CONF_CHARGE): cv.maybe_simple_value( sensor.sensor_schema( - unit_of_measurement=UNIT_AMPERE_HOURS, + unit_of_measurement=UNIT_AMPERE_HOUR, accuracy_decimals=8, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 7185d21810..de01c99a19 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -79,8 +79,6 @@ void INA2XX::setup() { this->state_ = State::IDLE; } -float INA2XX::get_setup_priority() const { return setup_priority::DATA; } - void INA2XX::update() { ESP_LOGD(TAG, "Updating"); if (this->is_ready() && this->state_ == State::IDLE) { diff --git a/esphome/components/ina2xx_base/ina2xx_base.h b/esphome/components/ina2xx_base/ina2xx_base.h index ba0999b28e..104c384a0d 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.h +++ b/esphome/components/ina2xx_base/ina2xx_base.h @@ -114,7 +114,6 @@ enum INAModel : uint8_t { INA_UNKNOWN = 0, INA_228, INA_229, INA_238, INA_239, I class INA2XX : public PollingComponent { public: void setup() override; - float get_setup_priority() const override; void update() override; void loop() override; void dump_config() override; diff --git a/esphome/components/ina3221/ina3221.cpp b/esphome/components/ina3221/ina3221.cpp index 8243764147..d03183e002 100644 --- a/esphome/components/ina3221/ina3221.cpp +++ b/esphome/components/ina3221/ina3221.cpp @@ -113,7 +113,6 @@ void INA3221Component::update() { } } -float INA3221Component::get_setup_priority() const { return setup_priority::DATA; } void INA3221Component::set_shunt_resistance(int channel, float resistance_ohm) { this->channels_[channel].shunt_resistance_ = resistance_ohm; } diff --git a/esphome/components/ina3221/ina3221.h b/esphome/components/ina3221/ina3221.h index f593badc09..3769df77aa 100644 --- a/esphome/components/ina3221/ina3221.h +++ b/esphome/components/ina3221/ina3221.h @@ -12,7 +12,6 @@ class INA3221Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_bus_voltage_sensor(int channel, sensor::Sensor *obj) { this->channels_[channel].bus_voltage_sensor_ = obj; } void set_shunt_voltage_sensor(int channel, sensor::Sensor *obj) { diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp index e5fa035682..534939f9af 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.cpp @@ -30,8 +30,6 @@ void KamstrupKMPComponent::dump_config() { this->check_uart_settings(1200, 2, uart::UART_CONFIG_PARITY_NONE, 8); } -float KamstrupKMPComponent::get_setup_priority() const { return setup_priority::DATA; } - void KamstrupKMPComponent::update() { if (this->heat_energy_sensor_ != nullptr) { this->command_queue_.push(CMD_HEAT_ENERGY); diff --git a/esphome/components/kamstrup_kmp/kamstrup_kmp.h b/esphome/components/kamstrup_kmp/kamstrup_kmp.h index c9cc9c5a39..f84e360132 100644 --- a/esphome/components/kamstrup_kmp/kamstrup_kmp.h +++ b/esphome/components/kamstrup_kmp/kamstrup_kmp.h @@ -83,7 +83,6 @@ class KamstrupKMPComponent : public PollingComponent, public uart::UARTDevice { void set_flow_sensor(sensor::Sensor *sensor) { this->flow_sensor_ = sensor; } void set_volume_sensor(sensor::Sensor *sensor) { this->volume_sensor_ = sensor; } void dump_config() override; - float get_setup_priority() const override; void update() override; void loop() override; void add_custom_sensor(sensor::Sensor *sensor, uint16_t command) { diff --git a/esphome/components/key_collector/__init__.py b/esphome/components/key_collector/__init__.py index 17af40da1a..badb28c32c 100644 --- a/esphome/components/key_collector/__init__.py +++ b/esphome/components/key_collector/__init__.py @@ -1,4 +1,7 @@ +from dataclasses import dataclass + from esphome import automation +from esphome.automation import Trigger import esphome.codegen as cg from esphome.components import key_provider import esphome.config_validation as cv @@ -10,7 +13,10 @@ from esphome.const import ( CONF_ON_TIMEOUT, CONF_SOURCE_ID, CONF_TIMEOUT, + CONF_TRIGGER_ID, ) +from esphome.cpp_generator import MockObj, literal +from esphome.types import TemplateArgsType CODEOWNERS = ["@ssieb"] @@ -32,22 +38,50 @@ KeyCollector = key_collector_ns.class_("KeyCollector", cg.Component) EnableAction = key_collector_ns.class_("EnableAction", automation.Action) DisableAction = key_collector_ns.class_("DisableAction", automation.Action) +X_TYPE = cg.std_string_ref.operator("const") + + +@dataclass +class Argument: + type: MockObj + name: str + + +TRIGGER_TYPES = { + CONF_ON_PROGRESS: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], + CONF_ON_RESULT: [ + Argument(X_TYPE, "x"), + Argument(cg.uint8, "start"), + Argument(cg.uint8, "end"), + ], + CONF_ON_TIMEOUT: [Argument(X_TYPE, "x"), Argument(cg.uint8, "start")], +} + CONFIG_SCHEMA = cv.All( cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(KeyCollector), - cv.Optional(CONF_SOURCE_ID): cv.use_id(key_provider.KeyProvider), - cv.Optional(CONF_MIN_LENGTH): cv.int_, - cv.Optional(CONF_MAX_LENGTH): cv.int_, + cv.Optional(CONF_SOURCE_ID): cv.ensure_list( + cv.use_id(key_provider.KeyProvider) + ), + cv.Optional(CONF_MIN_LENGTH): cv.uint16_t, + cv.Optional(CONF_MAX_LENGTH): cv.uint16_t, cv.Optional(CONF_START_KEYS): cv.string, cv.Optional(CONF_END_KEYS): cv.string, cv.Optional(CONF_END_KEY_REQUIRED): cv.boolean, cv.Optional(CONF_BACK_KEYS): cv.string, cv.Optional(CONF_CLEAR_KEYS): cv.string, cv.Optional(CONF_ALLOWED_KEYS): cv.string, - cv.Optional(CONF_ON_PROGRESS): automation.validate_automation(single=True), - cv.Optional(CONF_ON_RESULT): automation.validate_automation(single=True), - cv.Optional(CONF_ON_TIMEOUT): automation.validate_automation(single=True), + **{ + cv.Optional(trigger_type): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( + Trigger.template(*[arg.type for arg in args]) + ), + } + ) + for trigger_type, args in TRIGGER_TYPES.items() + }, cv.Optional(CONF_TIMEOUT): cv.positive_time_period_milliseconds, cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean, } @@ -59,9 +93,9 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) - if CONF_SOURCE_ID in config: - source = await cg.get_variable(config[CONF_SOURCE_ID]) - cg.add(var.set_provider(source)) + for source_conf in config.get(CONF_SOURCE_ID, ()): + source = await cg.get_variable(source_conf) + cg.add(var.add_provider(source)) if CONF_MIN_LENGTH in config: cg.add(var.set_min_length(config[CONF_MIN_LENGTH])) if CONF_MAX_LENGTH in config: @@ -78,26 +112,25 @@ async def to_code(config): cg.add(var.set_clear_keys(config[CONF_CLEAR_KEYS])) if CONF_ALLOWED_KEYS in config: cg.add(var.set_allowed_keys(config[CONF_ALLOWED_KEYS])) - if CONF_ON_PROGRESS in config: - await automation.build_automation( - var.get_progress_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_PROGRESS], - ) - if CONF_ON_RESULT in config: - await automation.build_automation( - var.get_result_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start"), (cg.uint8, "end")], - config[CONF_ON_RESULT], - ) - if CONF_ON_TIMEOUT in config: - await automation.build_automation( - var.get_timeout_trigger(), - [(cg.std_string, "x"), (cg.uint8, "start")], - config[CONF_ON_TIMEOUT], - ) - if CONF_TIMEOUT in config: - cg.add(var.set_timeout(config[CONF_TIMEOUT])) + + for trigger_name, args in TRIGGER_TYPES.items(): + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + for conf in config.get(trigger_name, ()): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) + add_trig = getattr( + var, + f"add_on_{trigger_name.rsplit('_', maxsplit=1)[-1].lower()}_callback", + ) + await automation.build_automation( + trigger, + arglist, + conf, + ) + lamb = trigger.trigger(*[literal(arg.name) for arg in args]) + cg.add(add_trig(await cg.process_lambda(lamb, arglist))) + + if timeout := config.get(CONF_TIMEOUT): + cg.add(var.set_timeout(timeout)) cg.add(var.set_enabled(config[CONF_ENABLE_ON_BOOT])) diff --git a/esphome/components/key_collector/key_collector.cpp b/esphome/components/key_collector/key_collector.cpp index 9cfc74f50e..68d1c60bf9 100644 --- a/esphome/components/key_collector/key_collector.cpp +++ b/esphome/components/key_collector/key_collector.cpp @@ -7,15 +7,10 @@ namespace key_collector { static const char *const TAG = "key_collector"; -KeyCollector::KeyCollector() - : progress_trigger_(new Trigger()), - result_trigger_(new Trigger()), - timeout_trigger_(new Trigger()) {} - void KeyCollector::loop() { if ((this->timeout_ == 0) || this->result_.empty() || (millis() - this->last_key_time_ < this->timeout_)) return; - this->timeout_trigger_->trigger(this->result_, this->start_key_); + this->timeout_callbacks_.call(this->result_, this->start_key_); this->clear(); } @@ -43,64 +38,68 @@ void KeyCollector::dump_config() { ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0); } -void KeyCollector::set_provider(key_provider::KeyProvider *provider) { - provider->add_on_key_callback([this](uint8_t key) { this->key_pressed_(key); }); +void KeyCollector::add_provider(key_provider::KeyProvider *provider) { + provider->add_on_key_callback([this](uint8_t key) { this->send_key(key); }); } void KeyCollector::set_enabled(bool enabled) { this->enabled_ = enabled; - if (!enabled) + if (!enabled) { this->clear(false); + } } void KeyCollector::clear(bool progress_update) { + auto had_state = !this->result_.empty() || this->start_key_ != 0; this->result_.clear(); this->start_key_ = 0; - if (progress_update) - this->progress_trigger_->trigger(this->result_, 0); + if (progress_update && had_state) { + this->progress_callbacks_.call(this->result_, 0); + } + this->disable_loop(); } -void KeyCollector::send_key(uint8_t key) { this->key_pressed_(key); } - -void KeyCollector::key_pressed_(uint8_t key) { +void KeyCollector::send_key(uint8_t key) { if (!this->enabled_) return; this->last_key_time_ = millis(); if (!this->start_keys_.empty() && !this->start_key_) { if (this->start_keys_.find(key) != std::string::npos) { this->start_key_ = key; - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->back_keys_.find(key) != std::string::npos) { if (!this->result_.empty()) { this->result_.pop_back(); - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } return; } if (this->clear_keys_.find(key) != std::string::npos) { - if (!this->result_.empty()) - this->clear(); + this->clear(); return; } if (this->end_keys_.find(key) != std::string::npos) { if ((this->min_length_ == 0) || (this->result_.size() >= this->min_length_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, key); + this->result_callbacks_.call(this->result_, this->start_key_, key); this->clear(); } return; } - if (!this->allowed_keys_.empty() && (this->allowed_keys_.find(key) == std::string::npos)) + if (!this->allowed_keys_.empty() && this->allowed_keys_.find(key) == std::string::npos) return; - if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) + if ((this->max_length_ == 0) || (this->result_.size() < this->max_length_)) { + if (this->result_.empty()) + this->enable_loop(); this->result_.push_back(key); + } if ((this->max_length_ > 0) && (this->result_.size() == this->max_length_) && (!this->end_key_required_)) { - this->result_trigger_->trigger(this->result_, this->start_key_, 0); + this->result_callbacks_.call(this->result_, this->start_key_, 0); this->clear(false); } - this->progress_trigger_->trigger(this->result_, this->start_key_); + this->progress_callbacks_.call(this->result_, this->start_key_); } } // namespace key_collector diff --git a/esphome/components/key_collector/key_collector.h b/esphome/components/key_collector/key_collector.h index 735f396809..8e30c333df 100644 --- a/esphome/components/key_collector/key_collector.h +++ b/esphome/components/key_collector/key_collector.h @@ -3,27 +3,33 @@ #include #include "esphome/components/key_provider/key_provider.h" #include "esphome/core/automation.h" +#include "esphome/core/helpers.h" namespace esphome { namespace key_collector { class KeyCollector : public Component { public: - KeyCollector(); void loop() override; void dump_config() override; - void set_provider(key_provider::KeyProvider *provider); - void set_min_length(uint32_t min_length) { this->min_length_ = min_length; }; - void set_max_length(uint32_t max_length) { this->max_length_ = max_length; }; + void add_provider(key_provider::KeyProvider *provider); + void set_min_length(uint16_t min_length) { this->min_length_ = min_length; }; + void set_max_length(uint16_t max_length) { this->max_length_ = max_length; }; void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); }; void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); }; void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; }; void set_back_keys(std::string back_keys) { this->back_keys_ = std::move(back_keys); }; void set_clear_keys(std::string clear_keys) { this->clear_keys_ = std::move(clear_keys); }; void set_allowed_keys(std::string allowed_keys) { this->allowed_keys_ = std::move(allowed_keys); }; - Trigger *get_progress_trigger() const { return this->progress_trigger_; }; - Trigger *get_result_trigger() const { return this->result_trigger_; }; - Trigger *get_timeout_trigger() const { return this->timeout_trigger_; }; + void add_on_progress_callback(std::function &&callback) { + this->progress_callbacks_.add(std::move(callback)); + } + void add_on_result_callback(std::function &&callback) { + this->result_callbacks_.add(std::move(callback)); + } + void add_on_timeout_callback(std::function &&callback) { + this->timeout_callbacks_.add(std::move(callback)); + } void set_timeout(int timeout) { this->timeout_ = timeout; }; void set_enabled(bool enabled); @@ -31,10 +37,8 @@ class KeyCollector : public Component { void send_key(uint8_t key); protected: - void key_pressed_(uint8_t key); - - uint32_t min_length_{0}; - uint32_t max_length_{0}; + uint16_t min_length_{0}; + uint16_t max_length_{0}; std::string start_keys_; std::string end_keys_; bool end_key_required_{false}; @@ -43,12 +47,12 @@ class KeyCollector : public Component { std::string allowed_keys_; std::string result_; uint8_t start_key_{0}; - Trigger *progress_trigger_; - Trigger *result_trigger_; - Trigger *timeout_trigger_; - uint32_t last_key_time_; + LazyCallbackManager progress_callbacks_; + LazyCallbackManager result_callbacks_; + LazyCallbackManager timeout_callbacks_; + uint32_t last_key_time_{}; uint32_t timeout_{0}; - bool enabled_; + bool enabled_{}; }; template class EnableAction : public Action, public Parented { diff --git a/esphome/components/key_collector/text_sensor/__init__.py b/esphome/components/key_collector/text_sensor/__init__.py new file mode 100644 index 0000000000..1676cf7bdf --- /dev/null +++ b/esphome/components/key_collector/text_sensor/__init__.py @@ -0,0 +1,28 @@ +import esphome.codegen as cg +from esphome.components import text_sensor +from esphome.components.text_sensor import TextSensor +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.cpp_generator import literal +from esphome.types import TemplateArgsType + +from .. import CONF_ON_RESULT, CONF_SOURCE_ID, TRIGGER_TYPES, KeyCollector + +CONFIG_SCHEMA = text_sensor.text_sensor_schema(TextSensor).extend( + { + cv.GenerateID(CONF_SOURCE_ID): cv.use_id(KeyCollector), + } +) + + +async def to_code(config): + parent = await cg.get_variable(config[CONF_SOURCE_ID]) + var = cg.new_Pvariable(config[CONF_ID]) + await text_sensor.register_text_sensor(var, config) + args = TRIGGER_TYPES[CONF_ON_RESULT] + arglist: TemplateArgsType = [(arg.type, arg.name) for arg in args] + cg.add( + parent.add_on_result_callback( + await cg.process_lambda(var.publish_state(literal(args[0].name)), arglist) + ) + ) diff --git a/esphome/components/kmeteriso/kmeteriso.cpp b/esphome/components/kmeteriso/kmeteriso.cpp index 36f6d74ba0..186686e472 100644 --- a/esphome/components/kmeteriso/kmeteriso.cpp +++ b/esphome/components/kmeteriso/kmeteriso.cpp @@ -47,8 +47,6 @@ void KMeterISOComponent::setup() { } } -float KMeterISOComponent::get_setup_priority() const { return setup_priority::DATA; } - void KMeterISOComponent::update() { uint8_t read_buf[4]; diff --git a/esphome/components/kmeteriso/kmeteriso.h b/esphome/components/kmeteriso/kmeteriso.h index c8bed662b0..6f1978105f 100644 --- a/esphome/components/kmeteriso/kmeteriso.h +++ b/esphome/components/kmeteriso/kmeteriso.h @@ -17,7 +17,6 @@ class KMeterISOComponent : public PollingComponent, public i2c::I2CDevice { // ========== INTERNAL METHODS ========== // (In most use cases you won't need these) void setup() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/libretiny/preferences.cpp b/esphome/components/libretiny/preferences.cpp index 978dcce3fa..5a60b535da 100644 --- a/esphome/components/libretiny/preferences.cpp +++ b/esphome/components/libretiny/preferences.cpp @@ -189,10 +189,11 @@ class LibreTinyPreferences : public ESPPreferences { } }; +static LibreTinyPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new LibreTinyPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->open(); - global_preferences = prefs; + s_preferences.open(); + global_preferences = &s_preferences; } } // namespace libretiny diff --git a/esphome/components/light/base_light_effects.h b/esphome/components/light/base_light_effects.h index 2eeae574e7..cdb9f1f666 100644 --- a/esphome/components/light/base_light_effects.h +++ b/esphome/components/light/base_light_effects.h @@ -138,20 +138,20 @@ class LambdaLightEffect : public LightEffect { class AutomationLightEffect : public LightEffect { public: AutomationLightEffect(const char *name) : LightEffect(name) {} - void stop() override { this->trig_->stop_action(); } + void stop() override { this->trig_.stop_action(); } void apply() override { - if (!this->trig_->is_action_running()) { - this->trig_->trigger(); + if (!this->trig_.is_action_running()) { + this->trig_.trigger(); } } - Trigger<> *get_trig() const { return trig_; } + Trigger<> *get_trig() { return &this->trig_; } /// Get the current effect index for use in automations. /// Useful for automations that need to know which effect is running. uint32_t get_current_index() const { return this->get_index(); } protected: - Trigger<> *trig_{new Trigger<>}; + Trigger<> trig_; }; struct StrobeLightEffectColor { diff --git a/esphome/components/light/light_call.cpp b/esphome/components/light/light_call.cpp index 6d42dd1513..3b4e136ba5 100644 --- a/esphome/components/light/light_call.cpp +++ b/esphome/components/light/light_call.cpp @@ -4,6 +4,7 @@ #include "light_state.h" #include "esphome/core/log.h" #include "esphome/core/optional.h" +#include "esphome/core/progmem.h" namespace esphome::light { @@ -51,26 +52,13 @@ static void log_invalid_parameter(const char *name, const LogString *message) { return *this; \ } +// Color mode human-readable strings indexed by ColorModeBitPolicy::to_bit() (0-9) +// Index 0 is Unknown (for ColorMode::UNKNOWN), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature", + "Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white"); + static const LogString *color_mode_to_human(ColorMode color_mode) { - if (color_mode == ColorMode::ON_OFF) - return LOG_STR("On/Off"); - if (color_mode == ColorMode::BRIGHTNESS) - return LOG_STR("Brightness"); - if (color_mode == ColorMode::WHITE) - return LOG_STR("White"); - if (color_mode == ColorMode::COLOR_TEMPERATURE) - return LOG_STR("Color temperature"); - if (color_mode == ColorMode::COLD_WARM_WHITE) - return LOG_STR("Cold/warm white"); - if (color_mode == ColorMode::RGB) - return LOG_STR("RGB"); - if (color_mode == ColorMode::RGB_WHITE) - return LOG_STR("RGBW"); - if (color_mode == ColorMode::RGB_COLD_WARM_WHITE) - return LOG_STR("RGB + cold/warm white"); - if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE) - return LOG_STR("RGB + color temperature"); - return LOG_STR("Unknown"); + return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0); } // Helper to log percentage values @@ -457,6 +445,52 @@ ColorMode LightCall::compute_color_mode_() { LOG_STR_ARG(color_mode_to_human(color_mode))); return color_mode; } +// PROGMEM lookup table for get_suitable_color_modes_mask_(). +// Maps 4-bit key (white | ct<<1 | cwww<<2 | rgb<<3) to color mode bitmask. +// Packed into uint8_t by right-shifting by PACK_SHIFT since the lower bits +// (UNKNOWN, ON_OFF, BRIGHTNESS) are never present in suitable mode masks. +static constexpr unsigned PACK_SHIFT = ColorModeBitPolicy::to_bit(ColorMode::WHITE); +// clang-format off +static constexpr uint8_t SUITABLE_COLOR_MODES[] PROGMEM = { + // [0] none - all modes with brightness + static_cast(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE, ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [1] white only + static_cast(ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [2] ct only + static_cast(ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [3] white + ct + static_cast(ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [4] cwww only + static_cast(ColorModeMask({ColorMode::COLD_WARM_WHITE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [5] white + cwww (conflicting) + 0, // [6] ct + cwww (conflicting) + 0, // [7] white + ct + cwww (conflicting) + // [8] rgb only + static_cast(ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [9] rgb + white + static_cast(ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [10] rgb + ct + static_cast(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [11] rgb + white + ct + static_cast(ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, + ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + // [12] rgb + cwww + static_cast(ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask() >> PACK_SHIFT), + 0, // [13] rgb + white + cwww (conflicting) + 0, // [14] rgb + ct + cwww (conflicting) + 0, // [15] all (conflicting) +}; +// clang-format on + color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { bool has_white = this->has_white() && this->white_ > 0.0f; bool has_ct = this->has_color_temperature(); @@ -466,46 +500,8 @@ color_mode_bitmask_t LightCall::get_suitable_color_modes_mask_() { (this->has_red() || this->has_green() || this->has_blue()); // Build key from flags: [rgb][cwww][ct][white] -#define KEY(white, ct, cwww, rgb) ((white) << 0 | (ct) << 1 | (cwww) << 2 | (rgb) << 3) - - uint8_t key = KEY(has_white, has_ct, has_cwww, has_rgb); - - switch (key) { - case KEY(true, false, false, false): // white only - return ColorModeMask({ColorMode::WHITE, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, false): // ct only - return ColorModeMask({ColorMode::COLOR_TEMPERATURE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, true, false, false): // white + ct - return ColorModeMask( - {ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, false, true, false): // cwww only - return ColorModeMask({ColorMode::COLD_WARM_WHITE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, false): // none - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE, - ColorMode::RGB, ColorMode::WHITE, ColorMode::COLOR_TEMPERATURE, ColorMode::COLD_WARM_WHITE}) - .get_mask(); - case KEY(true, false, false, true): // rgb + white - return ColorModeMask({ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - case KEY(false, true, false, true): // rgb + ct - case KEY(true, true, false, true): // rgb + white + ct - return ColorModeMask({ColorMode::RGB_COLOR_TEMPERATURE, ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, true, true): // rgb + cwww - return ColorModeMask({ColorMode::RGB_COLD_WARM_WHITE}).get_mask(); - case KEY(false, false, false, true): // rgb only - return ColorModeMask({ColorMode::RGB, ColorMode::RGB_WHITE, ColorMode::RGB_COLOR_TEMPERATURE, - ColorMode::RGB_COLD_WARM_WHITE}) - .get_mask(); - default: - return 0; // conflicting flags - } - -#undef KEY + uint8_t key = has_white | (has_ct << 1) | (has_cwww << 2) | (has_rgb << 3); + return static_cast(progmem_read_byte(&SUITABLE_COLOR_MODES[key])) << PACK_SHIFT; } LightCall &LightCall::set_effect(const char *effect, size_t len) { diff --git a/esphome/components/light/light_json_schema.cpp b/esphome/components/light/light_json_schema.cpp index 631f59221f..aaa1176f9f 100644 --- a/esphome/components/light/light_json_schema.cpp +++ b/esphome/components/light/light_json_schema.cpp @@ -9,32 +9,19 @@ namespace esphome::light { // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema -// Get JSON string for color mode. -// ColorMode enum values are sparse bitmasks (0, 1, 3, 7, 11, 19, 35, 39, 47, 51) which would -// generate a large jump table. Converting to bit index (0-9) allows a compact switch. +// Color mode JSON strings - packed into flash with compile-time generated offsets. +// Indexed by ColorModeBitPolicy bit index (1-9), so index 0 maps to bit 1 ("onoff"). +PROGMEM_STRING_TABLE(ColorModeStrings, "onoff", "brightness", "white", "color_temp", "cwww", "rgb", "rgbw", "rgbct", + "rgbww"); + +// Get JSON string for color mode. Returns nullptr for UNKNOWN (bit 0). +// Returns ProgmemStr so ArduinoJson knows to handle PROGMEM strings on ESP8266. static ProgmemStr get_color_mode_json_str(ColorMode mode) { - switch (ColorModeBitPolicy::to_bit(mode)) { - case 1: - return ESPHOME_F("onoff"); - case 2: - return ESPHOME_F("brightness"); - case 3: - return ESPHOME_F("white"); - case 4: - return ESPHOME_F("color_temp"); - case 5: - return ESPHOME_F("cwww"); - case 6: - return ESPHOME_F("rgb"); - case 7: - return ESPHOME_F("rgbw"); - case 8: - return ESPHOME_F("rgbct"); - case 9: - return ESPHOME_F("rgbww"); - default: - return nullptr; - } + unsigned bit = ColorModeBitPolicy::to_bit(mode); + if (bit == 0) + return nullptr; + // bit is 1-9 for valid modes, so bit-1 is always valid (0-8). LAST_INDEX fallback never used. + return ColorModeStrings::get_progmem_str(bit - 1, ColorModeStrings::LAST_INDEX); } void LightJSONSchema::dump_json(LightState &state, JsonObject root) { diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 9fa1ba3600..939c84720b 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -2,27 +2,18 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::lock { static const char *const TAG = "lock"; +// Lock state strings indexed by LockState enum (0-5): NONE(UNKNOWN), LOCKED, UNLOCKED, JAMMED, LOCKING, UNLOCKING +// Index 0 is UNKNOWN (for LOCK_STATE_NONE), also used as fallback for out-of-range +PROGMEM_STRING_TABLE(LockStateStrings, "UNKNOWN", "LOCKED", "UNLOCKED", "JAMMED", "LOCKING", "UNLOCKING"); + const LogString *lock_state_to_string(LockState state) { - switch (state) { - case LOCK_STATE_LOCKED: - return LOG_STR("LOCKED"); - case LOCK_STATE_UNLOCKED: - return LOG_STR("UNLOCKED"); - case LOCK_STATE_JAMMED: - return LOG_STR("JAMMED"); - case LOCK_STATE_LOCKING: - return LOG_STR("LOCKING"); - case LOCK_STATE_UNLOCKING: - return LOG_STR("UNLOCKING"); - case LOCK_STATE_NONE: - default: - return LOG_STR("UNKNOWN"); - } + return LockStateStrings::get_log_str(static_cast(state), 0); } Lock::Lock() : state(LOCK_STATE_NONE) {} @@ -84,21 +75,21 @@ LockCall &LockCall::set_state(optional state) { this->state_ = state; return *this; } -LockCall &LockCall::set_state(const std::string &state) { - if (str_equals_case_insensitive(state, "LOCKED")) { +LockCall &LockCall::set_state(const char *state) { + if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKED")) == 0) { this->set_state(LOCK_STATE_LOCKED); - } else if (str_equals_case_insensitive(state, "UNLOCKED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKED")) == 0) { this->set_state(LOCK_STATE_UNLOCKED); - } else if (str_equals_case_insensitive(state, "JAMMED")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("JAMMED")) == 0) { this->set_state(LOCK_STATE_JAMMED); - } else if (str_equals_case_insensitive(state, "LOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("LOCKING")) == 0) { this->set_state(LOCK_STATE_LOCKING); - } else if (str_equals_case_insensitive(state, "UNLOCKING")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("UNLOCKING")) == 0) { this->set_state(LOCK_STATE_UNLOCKING); - } else if (str_equals_case_insensitive(state, "NONE")) { + } else if (ESPHOME_strcasecmp_P(state, ESPHOME_PSTR("NONE")) == 0) { this->set_state(LOCK_STATE_NONE); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized state %s", this->parent_->get_name().c_str(), state); } return *this; } diff --git a/esphome/components/lock/lock.h b/esphome/components/lock/lock.h index 69fc405713..bebd296eac 100644 --- a/esphome/components/lock/lock.h +++ b/esphome/components/lock/lock.h @@ -83,7 +83,8 @@ class LockCall { /// Set the state of the lock device. LockCall &set_state(optional state); /// Set the state of the lock device based on a string. - LockCall &set_state(const std::string &state); + LockCall &set_state(const char *state); + LockCall &set_state(const std::string &state) { return this->set_state(state.c_str()); } void perform(); diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index cadd0a14ae..40ceaec7dc 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -16,6 +16,8 @@ from esphome.components.esp32 import ( VARIANT_ESP32S3, add_idf_sdkconfig_option, get_esp32_variant, + require_usb_serial_jtag_secondary, + require_vfs_termios, ) from esphome.components.libretiny import get_libretiny_component, get_libretiny_family from esphome.components.libretiny.const import ( @@ -397,9 +399,15 @@ async def to_code(config): elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG: add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True) cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG") + # Define platform support flags for components that need auto-detection try: uart_selection(USB_SERIAL_JTAG) cg.add_define("USE_LOGGER_USB_SERIAL_JTAG") + # USB Serial JTAG code is compiled when platform supports it. + # Enable secondary USB serial JTAG console so the VFS functions are available. + if CORE.is_esp32 and config[CONF_HARDWARE_UART] != USB_SERIAL_JTAG: + require_usb_serial_jtag_secondary() + require_vfs_termios() except cv.Invalid: pass try: diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 3a726d4046..4cbd4f1bf1 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -4,6 +4,7 @@ #include "esphome/core/application.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::logger { @@ -36,9 +37,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Fast path: main thread, no recursion (99.9% of all logs) if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { - RecursionGuard guard(this->main_task_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); return; } @@ -101,12 +100,9 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety - uint16_t buffer_at = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at, - MAX_CONSOLE_LOG_MSG_SIZE); - // Add newline before writing to console - this->add_newline_to_buffer_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE); - this->write_msg_(console_buffer, buffer_at); + LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->write_to_console_(buf); } // RAII guard automatically resets on return @@ -117,9 +113,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - // Format and send to both console and callbacks - this->log_message_to_buffer_and_send_(level, tag, line, format, args); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY @@ -128,60 +122,14 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // // This function handles format strings stored in flash memory (PROGMEM) to save RAM. -// The buffer is used in a special way to avoid allocating extra memory: -// -// Memory layout during execution: -// Step 1: Copy format string from flash to buffer -// tx_buffer_: [format_string][null][.....................] -// tx_buffer_at_: ------------------^ -// msg_start: saved here -----------^ -// -// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning -// and writes formatted output starting at msg_start position -// tx_buffer_: [format_string][null][formatted_message][null] -// tx_buffer_at_: -------------------------------------^ -// -// Step 3: Output the formatted message (starting at msg_start) -// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start -// which points to: [formatted_message][null] +// Uses vsnprintf_P to read the format string directly from flash without copying to RAM. // void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; - RecursionGuard guard(global_recursion_guard_); - this->tx_buffer_at_ = 0; - - // Copy format string from progmem - auto *format_pgm_p = reinterpret_cast(format); - char ch = '.'; - while (this->tx_buffer_at_ < this->tx_buffer_size_ && ch != '\0') { - this->tx_buffer_[this->tx_buffer_at_++] = ch = (char) progmem_read_byte(format_pgm_p++); - } - - // Buffer full from copying format - RAII guard handles cleanup on return - if (this->tx_buffer_at_ >= this->tx_buffer_size_) { - return; - } - - // Save the offset before calling format_log_to_buffer_with_terminator_ - // since it will increment tx_buffer_at_ to the end of the formatted string - uint16_t msg_start = this->tx_buffer_at_; - this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_, - &this->tx_buffer_at_, this->tx_buffer_size_); - - uint16_t msg_length = - this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position - - // Listeners get message first (before console write) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_ + msg_start, msg_length); -#endif - - // Write to console starting at the msg_start - this->write_tx_buffer_to_console_(msg_start, &msg_length); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); } #endif // USE_STORE_LOG_STR_IN_FLASH @@ -240,10 +188,11 @@ void Logger::process_messages_() { logger::TaskLogBufferHost::LogMessage *message; while (this->log_buffer_->get_message_main_loop(&message)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, message->text, - message->text_length); + message->text_length, buf); this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_ESP32) logger::TaskLogBuffer::LogMessage *message; @@ -251,22 +200,24 @@ void Logger::process_messages_() { void *received_token; while (this->log_buffer_->borrow_message_main_loop(&message, &text, &received_token)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(received_token); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #elif defined(USE_LIBRETINY) logger::TaskLogBufferLibreTiny::LogMessage *message; const char *text; while (this->log_buffer_->borrow_message_main_loop(&message, &text)) { const char *thread_name = message->thread_name[0] != '\0' ? message->thread_name : nullptr; + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; this->format_buffered_message_and_notify_(message->level, message->tag, message->line, thread_name, text, - message->text_length); + message->text_length, buf); // Release the message to allow other tasks to use it as soon as possible this->log_buffer_->release_message_main_loop(); - this->write_tx_buffer_to_console_(); + this->write_log_buffer_to_console_(buf); } #endif } @@ -291,34 +242,20 @@ UARTSelection Logger::get_uart() const { return this->uart_; } float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; } -#ifdef USE_STORE_LOG_STR_IN_FLASH -// ESP8266: PSTR() cannot be used in array initializers, so we need to declare -// each string separately as a global constant first -static const char LOG_LEVEL_NONE[] PROGMEM = "NONE"; -static const char LOG_LEVEL_ERROR[] PROGMEM = "ERROR"; -static const char LOG_LEVEL_WARN[] PROGMEM = "WARN"; -static const char LOG_LEVEL_INFO[] PROGMEM = "INFO"; -static const char LOG_LEVEL_CONFIG[] PROGMEM = "CONFIG"; -static const char LOG_LEVEL_DEBUG[] PROGMEM = "DEBUG"; -static const char LOG_LEVEL_VERBOSE[] PROGMEM = "VERBOSE"; -static const char LOG_LEVEL_VERY_VERBOSE[] PROGMEM = "VERY_VERBOSE"; +// Log level strings - packed into flash on ESP8266, indexed by log level (0-7) +PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"); -static const LogString *const LOG_LEVELS[] = { - reinterpret_cast(LOG_LEVEL_NONE), reinterpret_cast(LOG_LEVEL_ERROR), - reinterpret_cast(LOG_LEVEL_WARN), reinterpret_cast(LOG_LEVEL_INFO), - reinterpret_cast(LOG_LEVEL_CONFIG), reinterpret_cast(LOG_LEVEL_DEBUG), - reinterpret_cast(LOG_LEVEL_VERBOSE), reinterpret_cast(LOG_LEVEL_VERY_VERBOSE), -}; -#else -static const char *const LOG_LEVELS[] = {"NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE"}; -#endif +static const LogString *get_log_level_str(uint8_t level) { + return LogLevelStrings::get_log_str(level, LogLevelStrings::LAST_INDEX); +} void Logger::dump_config() { ESP_LOGCONFIG(TAG, "Logger:\n" " Max Level: %s\n" " Initial Level: %s", - LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL]), LOG_STR_ARG(LOG_LEVELS[this->current_level_])); + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL)), + LOG_STR_ARG(get_log_level_str(this->current_level_))); #ifndef USE_HOST ESP_LOGCONFIG(TAG, " Log Baud Rate: %" PRIu32 "\n" @@ -337,7 +274,7 @@ void Logger::dump_config() { #ifdef USE_LOGGER_RUNTIME_TAG_LEVELS for (auto &it : this->log_levels_) { - ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(LOG_LEVELS[it.second])); + ESP_LOGCONFIG(TAG, " Level for '%s': %s", it.first, LOG_STR_ARG(get_log_level_str(it.second))); } #endif } @@ -345,7 +282,8 @@ void Logger::dump_config() { void Logger::set_log_level(uint8_t level) { if (level > ESPHOME_LOG_LEVEL) { level = ESPHOME_LOG_LEVEL; - ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", LOG_STR_ARG(LOG_LEVELS[ESPHOME_LOG_LEVEL])); + ESP_LOGW(TAG, "Cannot set log level higher than pre-compiled %s", + LOG_STR_ARG(get_log_level_str(ESPHOME_LOG_LEVEL))); } this->current_level_ = level; #ifdef USE_LOGGER_LEVEL_LISTENERS diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index fe9cab4993..1678fed5f5 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,6 +2,7 @@ #include #include +#include #if defined(USE_ESP32) || defined(USE_HOST) #include #endif @@ -123,6 +124,163 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Buffer wrapper for log formatting functions +struct LogBuffer { + char *data; + uint16_t size; + uint16_t pos{0}; + // Replaces the null terminator with a newline for console output. + // Must be called after notify_listeners_() since listeners need null-terminated strings. + // Console output uses length-based writes (buf.pos), so null terminator is not needed. + void terminate_with_newline() { + if (this->pos < this->size) { + this->data[this->pos++] = '\n'; + } else if (this->size > 0) { + // Buffer was full - replace last char with newline to ensure it's visible + this->data[this->size - 1] = '\n'; + this->pos = this->size; + } + } + void HOT write_header(uint8_t level, const char *tag, int line, const char *thread_name) { + // Early return if insufficient space - intentionally don't update pos to prevent partial writes + if (this->pos + MAX_HEADER_SIZE > this->size) + return; + + char *p = this->current_(); + + // Write ANSI color + this->write_ansi_color_(p, level); + + // Construct: [LEVEL][tag:line] + *p++ = '['; + if (level != 0) { + if (level >= 7) { + *p++ = 'V'; // VERY_VERBOSE = "VV" + *p++ = 'V'; + } else { + *p++ = LOG_LEVEL_LETTER_CHARS[level]; + } + } + *p++ = ']'; + *p++ = '['; + + // Copy tag + this->copy_string_(p, tag); + + *p++ = ':'; + + // Format line number without modulo operations + if (line > 999) [[unlikely]] { + int thousands = line / 1000; + *p++ = '0' + thousands; + line -= thousands * 1000; + } + int hundreds = line / 100; + int remainder = line - hundreds * 100; + int tens = remainder / 10; + *p++ = '0' + hundreds; + *p++ = '0' + tens; + *p++ = '0' + (remainder - tens * 10); + *p++ = ']'; + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) + // Write thread name with bold red color + if (thread_name != nullptr) { + this->write_ansi_color_(p, 1); // Bold red for thread name + *p++ = '['; + this->copy_string_(p, thread_name); + *p++ = ']'; + this->write_ansi_color_(p, level); // Restore original color + } +#endif + + *p++ = ':'; + *p++ = ' '; + + this->pos = p - this->data; + } + void HOT format_body(const char *format, va_list args) { + this->format_vsnprintf_(format, args); + this->finalize_(); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void HOT format_body_P(PGM_P format, va_list args) { + this->format_vsnprintf_P_(format, args); + this->finalize_(); + } +#endif + void write_body(const char *text, uint16_t text_length) { + this->write_(text, text_length); + this->finalize_(); + } + + private: + bool full_() const { return this->pos >= this->size; } + uint16_t remaining_() const { return this->size - this->pos; } + char *current_() { return this->data + this->pos; } + void write_(const char *value, uint16_t length) { + const uint16_t available = this->remaining_(); + const uint16_t copy_len = (length < available) ? length : available; + if (copy_len > 0) { + memcpy(this->current_(), value, copy_len); + this->pos += copy_len; + } + } + void finalize_() { + // Write color reset sequence + static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; + this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN); + // Null terminate + this->data[this->full_() ? this->size - 1 : this->pos] = '\0'; + } + void strip_trailing_newlines_() { + while (this->pos > 0 && this->data[this->pos - 1] == '\n') + this->pos--; + } + void process_vsnprintf_result_(int ret) { + if (ret < 0) + return; + const uint16_t rem = this->remaining_(); + this->pos += (ret >= rem) ? (rem - 1) : static_cast(ret); + this->strip_trailing_newlines_(); + } + void format_vsnprintf_(const char *format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf(this->current_(), this->remaining_(), format, args)); + } +#ifdef USE_STORE_LOG_STR_IN_FLASH + void format_vsnprintf_P_(PGM_P format, va_list args) { + if (this->full_()) + return; + this->process_vsnprintf_result_(vsnprintf_P(this->current_(), this->remaining_(), format, args)); + } +#endif + // Write ANSI color escape sequence to buffer, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void write_ansi_color_(char *&p, uint8_t level) { + if (level == 0) + return; + // Direct buffer fill: "\033[{bold};3{color}m" (7 bytes) + *p++ = '\033'; + *p++ = '['; + *p++ = (level == 1) ? '1' : '0'; // Only ERROR is bold + *p++ = ';'; + *p++ = '3'; + *p++ = LOG_LEVEL_COLOR_DIGIT[level]; + *p++ = 'm'; + } + // Copy string without null terminator, updates pointer in place + // Caller is responsible for ensuring buffer has sufficient space + void copy_string_(char *&p, const char *str) { + const size_t len = strlen(str); + // NOLINTNEXTLINE(bugprone-not-null-terminated-result) - intentionally no null terminator, building string piece by + // piece + memcpy(p, str, len); + p += len; + } +}; + #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) /** Enum for logging UART selection * @@ -260,114 +418,83 @@ class Logger : public Component { #endif #endif void process_messages_(); - void write_msg_(const char *msg, size_t len); + void write_msg_(const char *msg, uint16_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator - // It's the caller's responsibility to initialize buffer_at (typically to 0) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, - va_list args, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { + va_list args, LogBuffer &buf) { #if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(), buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, this->get_thread_name_()); #elif defined(USE_ZEPHYR) - char buff[MAX_POINTER_REPRESENTATION]; - this->write_header_to_buffer_(level, tag, line, this->get_thread_name_(buff), buffer, buffer_at, buffer_size); + char tmp[MAX_POINTER_REPRESENTATION]; + buf.write_header(level, tag, line, this->get_thread_name_(tmp)); #else - this->write_header_to_buffer_(level, tag, line, nullptr, buffer, buffer_at, buffer_size); + buf.write_header(level, tag, line, nullptr); #endif - this->format_body_to_buffer_(buffer, buffer_at, buffer_size, format, args); - this->write_footer_to_buffer_(buffer, buffer_at, buffer_size); - - // Always ensure the buffer has a null terminator, even if we need to - // overwrite the last character of the actual content - if (*buffer_at >= buffer_size) { - buffer[buffer_size - 1] = '\0'; // Truncate and ensure null termination - } else { - buffer[*buffer_at] = '\0'; // Normal case, append null terminator - } + buf.format_body(format, args); } - // Helper to add newline to buffer before writing to console - // Modifies buffer_at to include the newline - inline void HOT add_newline_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - // Add newline - don't need to maintain null termination - // write_msg_ receives explicit length, so we can safely overwrite the null terminator - // This is safe because: - // 1. Callbacks already received the message (before we add newline) - // 2. write_msg_ receives the length explicitly (doesn't need null terminator) - if (*buffer_at < buffer_size) { - buffer[(*buffer_at)++] = '\n'; - } else if (buffer_size > 0) { - // Buffer was full - replace last char with newline to ensure it's visible - buffer[buffer_size - 1] = '\n'; - *buffer_at = buffer_size; - } +#ifdef USE_STORE_LOG_STR_IN_FLASH + // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator + inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, + const __FlashStringHelper *format, va_list args, + LogBuffer &buf) { + buf.write_header(level, tag, line, nullptr); + buf.format_body_P(reinterpret_cast(format), args); + } +#endif + + // Helper to notify log listeners + inline void HOT notify_listeners_(uint8_t level, const char *tag, const LogBuffer &buf) { +#ifdef USE_LOG_LISTENERS + for (auto *listener : this->log_listeners_) + listener->on_log(level, tag, buf.data, buf.pos); +#endif } - // Helper to write tx_buffer_ to console if logging is enabled - // INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null - inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) { - if (this->baud_rate_ > 0) { - uint16_t *len_ptr = length ? length : &this->tx_buffer_at_; - this->add_newline_to_buffer_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset); - this->write_msg_(this->tx_buffer_ + offset, *len_ptr); - } + // Helper to write log buffer to console (replaces null terminator with newline and writes) + inline void HOT write_to_console_(LogBuffer &buf) { + buf.terminate_with_newline(); + this->write_msg_(buf.data, buf.pos); + } + + // Helper to write log buffer to console if logging is enabled + inline void HOT write_log_buffer_to_console_(LogBuffer &buf) { + if (this->baud_rate_ > 0) + this->write_to_console_(buf); } // Helper to format and send a log message to both console and listeners - inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format, - va_list args) { - // Format to tx_buffer and prepare for output - this->tx_buffer_at_ = 0; // Initialize buffer position - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - - // Listeners get message WITHOUT newline (for API/MQTT/syslog) -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); + // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings + template + inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, + FormatType format, va_list args) { + RecursionGuard guard(recursion_guard); + LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; +#ifdef USE_STORE_LOG_STR_IN_FLASH + if constexpr (std::is_same_v) { + this->format_log_to_buffer_with_terminator_P_(level, tag, line, format, args, buf); + } else #endif - - // Console gets message WITH newline (if platform needs it) - this->write_tx_buffer_to_console_(); + { + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + } + this->notify_listeners_(level, tag, buf); + this->write_log_buffer_to_console_(buf); } #ifdef USE_ESPHOME_TASK_LOG_BUFFER // Helper to format a pre-formatted message from the task log buffer and notify listeners // Used by process_messages_ to avoid code duplication between ESP32 and host platforms inline void HOT format_buffered_message_and_notify_(uint8_t level, const char *tag, uint16_t line, - const char *thread_name, const char *text, size_t text_length) { - this->tx_buffer_at_ = 0; - this->write_header_to_buffer_(level, tag, line, thread_name, this->tx_buffer_, &this->tx_buffer_at_, - this->tx_buffer_size_); - this->write_body_to_buffer_(text, text_length, this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); - this->tx_buffer_[this->tx_buffer_at_] = '\0'; -#ifdef USE_LOG_LISTENERS - for (auto *listener : this->log_listeners_) - listener->on_log(level, tag, this->tx_buffer_, this->tx_buffer_at_); -#endif + const char *thread_name, const char *text, uint16_t text_length, + LogBuffer &buf) { + buf.write_header(level, tag, line, thread_name); + buf.write_body(text, text_length); + this->notify_listeners_(level, tag, buf); } #endif - // Write the body of the log message to the buffer - inline void write_body_to_buffer_(const char *value, size_t length, char *buffer, uint16_t *buffer_at, - uint16_t buffer_size) { - // Calculate available space - if (*buffer_at >= buffer_size) - return; - const uint16_t available = buffer_size - *buffer_at; - - // Determine copy length (minimum of remaining capacity and string length) - const size_t copy_len = (length < static_cast(available)) ? length : available; - - // Copy the data - if (copy_len > 0) { - memcpy(buffer + *buffer_at, value, copy_len); - *buffer_at += copy_len; - } - } - #ifndef USE_HOST const LogString *get_uart_selection_(); #endif @@ -421,7 +548,6 @@ class Logger : public Component { #endif // Group smaller types together at the end - uint16_t tx_buffer_at_{0}; uint16_t tx_buffer_size_{0}; uint8_t current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_ZEPHYR) @@ -525,108 +651,6 @@ class Logger : public Component { } #endif - static inline void copy_string(char *buffer, uint16_t &pos, const char *str) { - const size_t len = strlen(str); - // Intentionally no null terminator, building larger string - memcpy(buffer + pos, str, len); // NOLINT(bugprone-not-null-terminated-result) - pos += len; - } - - static inline void write_ansi_color_for_level(char *buffer, uint16_t &pos, uint8_t level) { - if (level == 0) - return; - // Construct ANSI escape sequence: "\033[{bold};3{color}m" - // Example: "\033[1;31m" for ERROR (bold red) - buffer[pos++] = '\033'; - buffer[pos++] = '['; - buffer[pos++] = (level == 1) ? '1' : '0'; // Only ERROR is bold - buffer[pos++] = ';'; - buffer[pos++] = '3'; - buffer[pos++] = LOG_LEVEL_COLOR_DIGIT[level]; - buffer[pos++] = 'm'; - } - - inline void HOT write_header_to_buffer_(uint8_t level, const char *tag, int line, const char *thread_name, - char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - uint16_t pos = *buffer_at; - // Early return if insufficient space - intentionally don't update buffer_at to prevent partial writes - if (pos + MAX_HEADER_SIZE > buffer_size) - return; - - // Construct: [LEVEL][tag:line]: - write_ansi_color_for_level(buffer, pos, level); - buffer[pos++] = '['; - if (level != 0) { - if (level >= 7) { - buffer[pos++] = 'V'; // VERY_VERBOSE = "VV" - buffer[pos++] = 'V'; - } else { - buffer[pos++] = LOG_LEVEL_LETTER_CHARS[level]; - } - } - buffer[pos++] = ']'; - buffer[pos++] = '['; - copy_string(buffer, pos, tag); - buffer[pos++] = ':'; - // Format line number without modulo operations (passed by value, safe to mutate) - if (line > 999) [[unlikely]] { - int thousands = line / 1000; - buffer[pos++] = '0' + thousands; - line -= thousands * 1000; - } - int hundreds = line / 100; - int remainder = line - hundreds * 100; - int tens = remainder / 10; - buffer[pos++] = '0' + hundreds; - buffer[pos++] = '0' + tens; - buffer[pos++] = '0' + (remainder - tens * 10); - buffer[pos++] = ']'; - -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) || defined(USE_HOST) - if (thread_name != nullptr) { - write_ansi_color_for_level(buffer, pos, 1); // Always use bold red for thread name - buffer[pos++] = '['; - copy_string(buffer, pos, thread_name); - buffer[pos++] = ']'; - write_ansi_color_for_level(buffer, pos, level); // Restore original color - } -#endif - - buffer[pos++] = ':'; - buffer[pos++] = ' '; - *buffer_at = pos; - } - - inline void HOT format_body_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size, const char *format, - va_list args) { - // Get remaining capacity in the buffer - if (*buffer_at >= buffer_size) - return; - const uint16_t remaining = buffer_size - *buffer_at; - - const int ret = vsnprintf(buffer + *buffer_at, remaining, format, args); - - if (ret < 0) { - return; // Encoding error, do not increment buffer_at - } - - // Update buffer_at with the formatted length (handle truncation) - // When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator - // When it doesn't truncate (ret < remaining), it writes ret chars + null terminator - uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret; - *buffer_at += formatted_len; - - // Remove all trailing newlines right after formatting - while (*buffer_at > 0 && buffer[*buffer_at - 1] == '\n') { - (*buffer_at)--; - } - } - - inline void HOT write_footer_to_buffer_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) { - static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1; - this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size); - } - #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { diff --git a/esphome/components/logger/logger_esp32.cpp b/esphome/components/logger/logger_esp32.cpp index 32ef752462..dfa643d5e9 100644 --- a/esphome/components/logger/logger_esp32.cpp +++ b/esphome/components/logger/logger_esp32.cpp @@ -114,16 +114,11 @@ void Logger::pre_setup() { global_logger = this; esp_log_set_vprintf(esp_idf_log_vprintf_); - if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE) { - esp_log_level_set("*", ESP_LOG_VERBOSE); - } ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { - // Length is now always passed explicitly - no strlen() fallback needed - +void HOT Logger::write_msg_(const char *msg, uint16_t len) { #if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG) // USB CDC/JTAG - single write including newline (already in buffer) // Use fwrite to stdout which goes through VFS to USB console diff --git a/esphome/components/logger/logger_esp8266.cpp b/esphome/components/logger/logger_esp8266.cpp index 6cee1baca5..0a3433d132 100644 --- a/esphome/components/logger/logger_esp8266.cpp +++ b/esphome/components/logger/logger_esp8266.cpp @@ -28,7 +28,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_host.cpp b/esphome/components/logger/logger_host.cpp index 874cdabd22..be12b6df6a 100644 --- a/esphome/components/logger/logger_host.cpp +++ b/esphome/components/logger/logger_host.cpp @@ -3,7 +3,7 @@ namespace esphome::logger { -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { static constexpr size_t TIMESTAMP_LEN = 10; // "[HH:MM:SS]" // tx_buffer_size_ defaults to 512, so 768 covers default + headroom char buffer[TIMESTAMP_LEN + 768]; @@ -15,7 +15,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { size_t pos = strftime(buffer, TIMESTAMP_LEN + 1, "[%H:%M:%S]", &timeinfo); // Copy message (with newline already included by caller) - size_t copy_len = std::min(len, sizeof(buffer) - pos); + size_t copy_len = std::min(static_cast(len), sizeof(buffer) - pos); memcpy(buffer + pos, msg, copy_len); pos += copy_len; diff --git a/esphome/components/logger/logger_libretiny.cpp b/esphome/components/logger/logger_libretiny.cpp index cdf55e710c..aab8a97abf 100644 --- a/esphome/components/logger/logger_libretiny.cpp +++ b/esphome/components/logger/logger_libretiny.cpp @@ -49,7 +49,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); } +void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); } const LogString *Logger::get_uart_selection_() { switch (this->uart_) { diff --git a/esphome/components/logger/logger_rp2040.cpp b/esphome/components/logger/logger_rp2040.cpp index be8252f56a..1f435031f6 100644 --- a/esphome/components/logger/logger_rp2040.cpp +++ b/esphome/components/logger/logger_rp2040.cpp @@ -27,7 +27,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) this->hw_serial_->write(msg, len); } diff --git a/esphome/components/logger/logger_zephyr.cpp b/esphome/components/logger/logger_zephyr.cpp index 41f53beec0..ef1702c5c1 100644 --- a/esphome/components/logger/logger_zephyr.cpp +++ b/esphome/components/logger/logger_zephyr.cpp @@ -63,7 +63,7 @@ void Logger::pre_setup() { ESP_LOGI(TAG, "Log initialized"); } -void HOT Logger::write_msg_(const char *msg, size_t len) { +void HOT Logger::write_msg_(const char *msg, uint16_t len) { // Single write with newline already in buffer (added by caller) #ifdef CONFIG_PRINTK // Requires the debug component and an active SWD connection. @@ -73,7 +73,7 @@ void HOT Logger::write_msg_(const char *msg, size_t len) { if (this->uart_dev_ == nullptr) { return; } - for (size_t i = 0; i < len; ++i) { + for (uint16_t i = 0; i < len; ++i) { uart_poll_out(this->uart_dev_, msg[i]); } } diff --git a/esphome/components/lvgl/automation.py b/esphome/components/lvgl/automation.py index 9b58727f2a..b589e42f3b 100644 --- a/esphome/components/lvgl/automation.py +++ b/esphome/components/lvgl/automation.py @@ -272,9 +272,7 @@ async def obj_hide_to_code(config, action_id, template_arg, args): async def do_hide(widget: Widget): widget.add_flag("LV_OBJ_FLAG_HIDDEN") - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_hide, action_id, template_arg, args) @@ -285,9 +283,7 @@ async def obj_show_to_code(config, action_id, template_arg, args): if widget.move_to_foreground: lv_obj.move_foreground(widget.obj) - widgets = [ - widget.outer if widget.outer else widget for widget in await get_widgets(config) - ] + widgets = [widget.outer or widget for widget in await get_widgets(config)] return await action_to_code(widgets, do_show, action_id, template_arg, args) diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.cpp b/esphome/components/m5stack_8angle/m5stack_8angle.cpp index 5a9a5e8c9d..2de900c21d 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.cpp +++ b/esphome/components/m5stack_8angle/m5stack_8angle.cpp @@ -69,7 +69,5 @@ int8_t M5Stack8AngleComponent::read_switch() { } } -float M5Stack8AngleComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace m5stack_8angle } // namespace esphome diff --git a/esphome/components/m5stack_8angle/m5stack_8angle.h b/esphome/components/m5stack_8angle/m5stack_8angle.h index 831b1422fd..4942518054 100644 --- a/esphome/components/m5stack_8angle/m5stack_8angle.h +++ b/esphome/components/m5stack_8angle/m5stack_8angle.h @@ -21,7 +21,6 @@ class M5Stack8AngleComponent : public i2c::I2CDevice, public Component { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; float read_knob_pos(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int32_t read_knob_pos_raw(uint8_t channel, AnalogBits bits = AnalogBits::BITS_8); int8_t read_switch(); diff --git a/esphome/components/max17043/max17043.cpp b/esphome/components/max17043/max17043.cpp index e8cf4d5ab1..dfd59f1e7d 100644 --- a/esphome/components/max17043/max17043.cpp +++ b/esphome/components/max17043/max17043.cpp @@ -81,8 +81,6 @@ void MAX17043Component::dump_config() { LOG_SENSOR(" ", "Battery Level", this->battery_remaining_sensor_); } -float MAX17043Component::get_setup_priority() const { return setup_priority::DATA; } - void MAX17043Component::sleep_mode() { if (!this->is_failed()) { if (!this->write_byte_16(MAX17043_CONFIG, MAX17043_CONFIG_POWER_UP_DEFAULT | MAX17043_CONFIG_SLEEP_MASK)) { diff --git a/esphome/components/max17043/max17043.h b/esphome/components/max17043/max17043.h index 540b977789..f477ce5948 100644 --- a/esphome/components/max17043/max17043.h +++ b/esphome/components/max17043/max17043.h @@ -11,7 +11,6 @@ class MAX17043Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void sleep_mode(); diff --git a/esphome/components/max31855/max31855.cpp b/esphome/components/max31855/max31855.cpp index b5be3106cf..99129880f4 100644 --- a/esphome/components/max31855/max31855.cpp +++ b/esphome/components/max31855/max31855.cpp @@ -31,7 +31,6 @@ void MAX31855Sensor::dump_config() { ESP_LOGCONFIG(TAG, " Reference temperature disabled."); } } -float MAX31855Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX31855Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max31855/max31855.h b/esphome/components/max31855/max31855.h index 822e256587..b755d240f2 100644 --- a/esphome/components/max31855/max31855.h +++ b/esphome/components/max31855/max31855.h @@ -18,7 +18,6 @@ class MAX31855Sensor : public sensor::Sensor, void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max31856/max31856.cpp b/esphome/components/max31856/max31856.cpp index cc573cbc53..ff65c8c5c9 100644 --- a/esphome/components/max31856/max31856.cpp +++ b/esphome/components/max31856/max31856.cpp @@ -197,7 +197,5 @@ uint32_t MAX31856Sensor::read_register24_(uint8_t reg) { return value; } -float MAX31856Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace max31856 } // namespace esphome diff --git a/esphome/components/max31856/max31856.h b/esphome/components/max31856/max31856.h index 8d64cfe8bc..a27ababa2e 100644 --- a/esphome/components/max31856/max31856.h +++ b/esphome/components/max31856/max31856.h @@ -76,7 +76,6 @@ class MAX31856Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_filter(MAX31856ConfigFilter filter) { this->filter_ = filter; } void set_thermocouple_type(MAX31856ThermocoupleType thermocouple_type) { this->thermocouple_type_ = thermocouple_type; diff --git a/esphome/components/max31865/max31865.cpp b/esphome/components/max31865/max31865.cpp index a9c5204cf5..09b8368d07 100644 --- a/esphome/components/max31865/max31865.cpp +++ b/esphome/components/max31865/max31865.cpp @@ -90,8 +90,6 @@ void MAX31865Sensor::dump_config() { (filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!"))); } -float MAX31865Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX31865Sensor::read_data_() { // Read temperature, disable V_BIAS (save power) const uint16_t rtd_resistance_register = this->read_register_16_(RTD_RESISTANCE_MSB_REG); diff --git a/esphome/components/max31865/max31865.h b/esphome/components/max31865/max31865.h index b83753a678..440c6523a6 100644 --- a/esphome/components/max31865/max31865.h +++ b/esphome/components/max31865/max31865.h @@ -34,7 +34,6 @@ class MAX31865Sensor : public sensor::Sensor, void set_num_rtd_wires(uint8_t rtd_wires) { rtd_wires_ = rtd_wires; } void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max44009/max44009.cpp b/esphome/components/max44009/max44009.cpp index 928fc47696..8b8e38c1ea 100644 --- a/esphome/components/max44009/max44009.cpp +++ b/esphome/components/max44009/max44009.cpp @@ -51,8 +51,6 @@ void MAX44009Sensor::dump_config() { } } -float MAX44009Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MAX44009Sensor::update() { // update sensor illuminance value float lux = this->read_illuminance_(); diff --git a/esphome/components/max44009/max44009.h b/esphome/components/max44009/max44009.h index c85d1c1028..59eea66ed9 100644 --- a/esphome/components/max44009/max44009.h +++ b/esphome/components/max44009/max44009.h @@ -16,7 +16,6 @@ class MAX44009Sensor : public sensor::Sensor, public PollingComponent, public i2 void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_mode(MAX44009Mode mode); bool set_continuous_mode(); diff --git a/esphome/components/max6675/max6675.cpp b/esphome/components/max6675/max6675.cpp index 54e0330ff7..c329cdfd42 100644 --- a/esphome/components/max6675/max6675.cpp +++ b/esphome/components/max6675/max6675.cpp @@ -23,7 +23,6 @@ void MAX6675Sensor::dump_config() { LOG_PIN(" CS Pin: ", this->cs_); LOG_UPDATE_INTERVAL(this); } -float MAX6675Sensor::get_setup_priority() const { return setup_priority::DATA; } void MAX6675Sensor::read_data_() { this->enable(); delay(1); diff --git a/esphome/components/max6675/max6675.h b/esphome/components/max6675/max6675.h index ab0f06b041..f0db4a6c26 100644 --- a/esphome/components/max6675/max6675.h +++ b/esphome/components/max6675/max6675.h @@ -14,7 +14,6 @@ class MAX6675Sensor : public sensor::Sensor, public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; diff --git a/esphome/components/max7219/display.py b/esphome/components/max7219/display.py index a434125148..abb20702bd 100644 --- a/esphome/components/max7219/display.py +++ b/esphome/components/max7219/display.py @@ -28,11 +28,10 @@ CONFIG_SCHEMA = ( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) + var = cg.new_Pvariable(config[CONF_ID], config[CONF_NUM_CHIPS]) await spi.register_spi_device(var, config, write_only=True) await display.register_display(var, config) - cg.add(var.set_num_chips(config[CONF_NUM_CHIPS])) cg.add(var.set_intensity(config[CONF_INTENSITY])) cg.add(var.set_reverse(config[CONF_REVERSE_ENABLE])) diff --git a/esphome/components/max7219/max7219.cpp b/esphome/components/max7219/max7219.cpp index 157b317c02..d701e6fc86 100644 --- a/esphome/components/max7219/max7219.cpp +++ b/esphome/components/max7219/max7219.cpp @@ -3,8 +3,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { static const char *const TAG = "max7219"; @@ -115,12 +114,14 @@ const uint8_t MAX7219_ASCII_TO_RAW[95] PROGMEM = { }; float MAX7219Component::get_setup_priority() const { return setup_priority::PROCESSOR; } + +MAX7219Component::MAX7219Component(uint8_t num_chips) : num_chips_(num_chips) { + this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT + memset(this->buffer_, 0, this->num_chips_ * 8); +} + void MAX7219Component::setup() { this->spi_setup(); - this->buffer_ = new uint8_t[this->num_chips_ * 8]; // NOLINT - for (uint8_t i = 0; i < this->num_chips_ * 8; i++) - this->buffer_[i] = 0; - // let's assume the user has all 8 digits connected, only important in daisy chained setups anyway this->send_to_all_(MAX7219_REGISTER_SCAN_LIMIT, 7); // let's use our own ASCII -> led pattern encoding @@ -229,7 +230,6 @@ void MAX7219Component::set_intensity(uint8_t intensity) { this->intensity_ = intensity; } } -void MAX7219Component::set_num_chips(uint8_t num_chips) { this->num_chips_ = num_chips; } uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time) { char buffer[64]; @@ -240,5 +240,4 @@ uint8_t MAX7219Component::strftime(uint8_t pos, const char *format, ESPTime time } uint8_t MAX7219Component::strftime(const char *format, ESPTime time) { return this->strftime(0, format, time); } -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 diff --git a/esphome/components/max7219/max7219.h b/esphome/components/max7219/max7219.h index 58d871d54c..ef38628f28 100644 --- a/esphome/components/max7219/max7219.h +++ b/esphome/components/max7219/max7219.h @@ -6,8 +6,7 @@ #include "esphome/components/spi/spi.h" #include "esphome/components/display/display.h" -namespace esphome { -namespace max7219 { +namespace esphome::max7219 { class MAX7219Component; @@ -17,6 +16,8 @@ class MAX7219Component : public PollingComponent, public spi::SPIDevice { public: + explicit MAX7219Component(uint8_t num_chips); + void set_writer(max7219_writer_t &&writer); void setup() override; @@ -30,7 +31,6 @@ class MAX7219Component : public PollingComponent, void display(); void set_intensity(uint8_t intensity); - void set_num_chips(uint8_t num_chips); void set_reverse(bool reverse) { this->reverse_ = reverse; }; /// Evaluate the printf-format and print the result at the given position. @@ -56,10 +56,9 @@ class MAX7219Component : public PollingComponent, uint8_t intensity_{15}; // Intensity of the display from 0 to 15 (most) bool intensity_changed_{}; // True if we need to re-send the intensity uint8_t num_chips_{1}; - uint8_t *buffer_; + uint8_t *buffer_{nullptr}; bool reverse_{false}; max7219_writer_t writer_{}; }; -} // namespace max7219 -} // namespace esphome +} // namespace esphome::max7219 diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp index 81eb0a812f..ee052e9fb7 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3008 { static const char *const TAG = "mcp3008.sensor"; -float MCP3008Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3008Sensor::dump_config() { ESP_LOGCONFIG(TAG, "MCP3008Sensor:\n" diff --git a/esphome/components/mcp3008/sensor/mcp3008_sensor.h b/esphome/components/mcp3008/sensor/mcp3008_sensor.h index ebaeab966f..9478d38e74 100644 --- a/esphome/components/mcp3008/sensor/mcp3008_sensor.h +++ b/esphome/components/mcp3008/sensor/mcp3008_sensor.h @@ -19,7 +19,6 @@ class MCP3008Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp index e673537be1..711448cf44 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.cpp @@ -7,8 +7,6 @@ namespace mcp3204 { static const char *const TAG = "mcp3204.sensor"; -float MCP3204Sensor::get_setup_priority() const { return setup_priority::DATA; } - void MCP3204Sensor::dump_config() { LOG_SENSOR("", "MCP3204 Sensor", this); ESP_LOGCONFIG(TAG, diff --git a/esphome/components/mcp3204/sensor/mcp3204_sensor.h b/esphome/components/mcp3204/sensor/mcp3204_sensor.h index 5665b80b98..2bf75a9c1e 100644 --- a/esphome/components/mcp3204/sensor/mcp3204_sensor.h +++ b/esphome/components/mcp3204/sensor/mcp3204_sensor.h @@ -19,7 +19,6 @@ class MCP3204Sensor : public PollingComponent, void update() override; void dump_config() override; - float get_setup_priority() const override; float sample() override; protected: diff --git a/esphome/components/mcp9808/mcp9808.cpp b/esphome/components/mcp9808/mcp9808.cpp index 088d33887f..ed12e52239 100644 --- a/esphome/components/mcp9808/mcp9808.cpp +++ b/esphome/components/mcp9808/mcp9808.cpp @@ -73,7 +73,6 @@ void MCP9808Sensor::update() { this->publish_state(temp); this->status_clear_warning(); } -float MCP9808Sensor::get_setup_priority() const { return setup_priority::DATA; } } // namespace mcp9808 } // namespace esphome diff --git a/esphome/components/mcp9808/mcp9808.h b/esphome/components/mcp9808/mcp9808.h index 19aa3117c3..894e4599d0 100644 --- a/esphome/components/mcp9808/mcp9808.h +++ b/esphome/components/mcp9808/mcp9808.h @@ -11,7 +11,6 @@ class MCP9808Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/mdns/mdns_esp32.cpp b/esphome/components/mdns/mdns_esp32.cpp index 3123f3b604..3e997402bc 100644 --- a/esphome/components/mdns/mdns_esp32.cpp +++ b/esphome/components/mdns/mdns_esp32.cpp @@ -12,6 +12,10 @@ namespace esphome::mdns { static const char *const TAG = "mdns"; static void register_esp32(MDNSComponent *comp, StaticVector &services) { +#ifdef USE_OPENTHREAD + // OpenThread handles service registration via SRP client + // Services are compiled by MDNSComponent::compile_records_() and consumed by OpenThreadSrpComponent +#else esp_err_t err = mdns_init(); if (err != ESP_OK) { ESP_LOGW(TAG, "Init failed: %s", esp_err_to_name(err)); @@ -41,13 +45,16 @@ static void register_esp32(MDNSComponent *comp, StaticVectorsetup_buffers_and_register_(register_esp32); } void MDNSComponent::on_shutdown() { +#ifndef USE_OPENTHREAD mdns_free(); delay(40); // Allow the mdns packets announcing service removal to be sent +#endif } } // namespace esphome::mdns diff --git a/esphome/components/media_player/media_player.cpp b/esphome/components/media_player/media_player.cpp index b46ec39d30..17d9b054da 100644 --- a/esphome/components/media_player/media_player.cpp +++ b/esphome/components/media_player/media_player.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace media_player { @@ -107,25 +108,25 @@ MediaPlayerCall &MediaPlayerCall::set_command(optional comma this->command_ = command; return *this; } -MediaPlayerCall &MediaPlayerCall::set_command(const std::string &command) { - if (str_equals_case_insensitive(command, "PLAY")) { +MediaPlayerCall &MediaPlayerCall::set_command(const char *command) { + if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PLAY")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PLAY); - } else if (str_equals_case_insensitive(command, "PAUSE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("PAUSE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_PAUSE); - } else if (str_equals_case_insensitive(command, "STOP")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("STOP")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_STOP); - } else if (str_equals_case_insensitive(command, "MUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("MUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_MUTE); - } else if (str_equals_case_insensitive(command, "UNMUTE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("UNMUTE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_UNMUTE); - } else if (str_equals_case_insensitive(command, "TOGGLE")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TOGGLE")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TOGGLE); - } else if (str_equals_case_insensitive(command, "TURN_ON")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_ON")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_ON); - } else if (str_equals_case_insensitive(command, "TURN_OFF")) { + } else if (ESPHOME_strcasecmp_P(command, ESPHOME_PSTR("TURN_OFF")) == 0) { this->set_command(MEDIA_PLAYER_COMMAND_TURN_OFF); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized command %s", this->parent_->get_name().c_str(), command); } return *this; } diff --git a/esphome/components/media_player/media_player.h b/esphome/components/media_player/media_player.h index b753e2d088..f75a68dd85 100644 --- a/esphome/components/media_player/media_player.h +++ b/esphome/components/media_player/media_player.h @@ -114,7 +114,8 @@ class MediaPlayerCall { MediaPlayerCall &set_command(MediaPlayerCommand command); MediaPlayerCall &set_command(optional command); - MediaPlayerCall &set_command(const std::string &command); + MediaPlayerCall &set_command(const char *command); + MediaPlayerCall &set_command(const std::string &command) { return this->set_command(command.c_str()); } MediaPlayerCall &set_media_url(const std::string &url); diff --git a/esphome/components/mhz19/mhz19.cpp b/esphome/components/mhz19/mhz19.cpp index b6b4031e16..bccea7d423 100644 --- a/esphome/components/mhz19/mhz19.cpp +++ b/esphome/components/mhz19/mhz19.cpp @@ -137,8 +137,6 @@ bool MHZ19Component::mhz19_write_command_(const uint8_t *command, uint8_t *respo return this->read_array(response, MHZ19_RESPONSE_LENGTH); } -float MHZ19Component::get_setup_priority() const { return setup_priority::DATA; } - void MHZ19Component::dump_config() { ESP_LOGCONFIG(TAG, "MH-Z19:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/mhz19/mhz19.h b/esphome/components/mhz19/mhz19.h index a27f1c31eb..e577b98537 100644 --- a/esphome/components/mhz19/mhz19.h +++ b/esphome/components/mhz19/mhz19.h @@ -22,8 +22,6 @@ enum MHZ19DetectionRange { class MHZ19Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void setup() override; void update() override; void dump_config() override; diff --git a/esphome/components/micro_wake_word/micro_wake_word.cpp b/esphome/components/micro_wake_word/micro_wake_word.cpp index d7e80efc84..b93bf1b556 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.cpp +++ b/esphome/components/micro_wake_word/micro_wake_word.cpp @@ -325,7 +325,7 @@ void MicroWakeWord::loop() { ESP_LOGD(TAG, "Detected '%s' with sliding average probability is %.2f and max probability is %.2f", detection_event.wake_word->c_str(), (detection_event.average_probability / uint8_to_float_divisor), (detection_event.max_probability / uint8_to_float_divisor)); - this->wake_word_detected_trigger_->trigger(*detection_event.wake_word); + this->wake_word_detected_trigger_.trigger(*detection_event.wake_word); if (this->stop_after_detection_) { this->stop(); } diff --git a/esphome/components/micro_wake_word/micro_wake_word.h b/esphome/components/micro_wake_word/micro_wake_word.h index b427e4dfcb..44d5d89372 100644 --- a/esphome/components/micro_wake_word/micro_wake_word.h +++ b/esphome/components/micro_wake_word/micro_wake_word.h @@ -60,7 +60,7 @@ class MicroWakeWord : public Component void set_stop_after_detection(bool stop_after_detection) { this->stop_after_detection_ = stop_after_detection; } - Trigger *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } + Trigger *get_wake_word_detected_trigger() { return &this->wake_word_detected_trigger_; } void add_wake_word_model(WakeWordModel *model); @@ -78,7 +78,7 @@ class MicroWakeWord : public Component protected: microphone::MicrophoneSource *microphone_source_{nullptr}; - Trigger *wake_word_detected_trigger_ = new Trigger(); + Trigger wake_word_detected_trigger_; State state_{State::STOPPED}; std::weak_ptr ring_buffer_; diff --git a/esphome/components/mics_4514/mics_4514.cpp b/esphome/components/mics_4514/mics_4514.cpp index 8181ece94c..60413b32d7 100644 --- a/esphome/components/mics_4514/mics_4514.cpp +++ b/esphome/components/mics_4514/mics_4514.cpp @@ -37,7 +37,6 @@ void MICS4514Component::dump_config() { LOG_SENSOR(" ", "Hydrogen", this->hydrogen_sensor_); LOG_SENSOR(" ", "Ammonia", this->ammonia_sensor_); } -float MICS4514Component::get_setup_priority() const { return setup_priority::DATA; } void MICS4514Component::update() { if (!this->warmed_up_) { return; diff --git a/esphome/components/mics_4514/mics_4514.h b/esphome/components/mics_4514/mics_4514.h index d2fefc3630..e7271314c8 100644 --- a/esphome/components/mics_4514/mics_4514.h +++ b/esphome/components/mics_4514/mics_4514.h @@ -19,7 +19,6 @@ class MICS4514Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/midea/climate.py b/esphome/components/midea/climate.py index b08a47afa9..8a3d4f22ba 100644 --- a/esphome/components/midea/climate.py +++ b/esphome/components/midea/climate.py @@ -30,7 +30,7 @@ from esphome.const import ( UNIT_PERCENT, UNIT_WATT, ) -from esphome.core import coroutine +from esphome.core import CORE, coroutine CODEOWNERS = ["@dudanov"] DEPENDENCIES = ["climate", "uart"] @@ -290,4 +290,7 @@ async def to_code(config): if CONF_HUMIDITY_SETPOINT in config: sens = await sensor.new_sensor(config[CONF_HUMIDITY_SETPOINT]) cg.add(var.set_humidity_setpoint_sensor(sens)) + # MideaUART library requires WiFi (WiFi auto-enables Network via dependency mapping) + if CORE.is_esp32: + cg.add_library("WiFi", None) cg.add_library("dudanov/MideaUART", "1.1.9") diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index c3d080f8b2..f83cacc5a3 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -94,3 +94,29 @@ DriverChip( (0x29, 0x00), ], ) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-7B", + height=600, + width=1024, + hsync_back_porch=160, + hsync_pulse_width=10, + hsync_front_porch=160, + vsync_back_porch=23, + vsync_pulse_width=1, + vsync_front_porch=12, + pclk_frequency="52MHz", + lane_bit_rate="900Mbps", + no_transform=True, + color_order="RGB", + initsequence=[ + (0x80, 0x8B), + (0x81, 0x78), + (0x82, 0x84), + (0x83, 0x88), + (0x84, 0xA8), + (0x85, 0xE3), + (0x86, 0x88), + (0xB2, 0x10), + ], +) diff --git a/esphome/components/mipi_spi/mipi_spi.cpp b/esphome/components/mipi_spi/mipi_spi.cpp index 272915b4e1..90f6324511 100644 --- a/esphome/components/mipi_spi/mipi_spi.cpp +++ b/esphome/components/mipi_spi/mipi_spi.cpp @@ -1,6 +1,39 @@ #include "mipi_spi.h" #include "esphome/core/log.h" -namespace esphome { -namespace mipi_spi {} // namespace mipi_spi -} // namespace esphome +namespace esphome::mipi_spi { + +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width) { + ESP_LOGCONFIG(TAG, + "MIPI_SPI Display\n" + " Model: %s\n" + " Width: %d\n" + " Height: %d\n" + " Swap X/Y: %s\n" + " Mirror X: %s\n" + " Mirror Y: %s\n" + " Invert colors: %s\n" + " Color order: %s\n" + " Display pixels: %d bits\n" + " Endianness: %s\n" + " SPI Mode: %d\n" + " SPI Data rate: %uMHz\n" + " SPI Bus width: %d", + model, width, height, YESNO(madctl & MADCTL_MV), YESNO(madctl & (MADCTL_MX | MADCTL_XFLIP)), + YESNO(madctl & (MADCTL_MY | MADCTL_YFLIP)), YESNO(invert_colors), (madctl & MADCTL_BGR) ? "BGR" : "RGB", + display_bits, is_big_endian ? "Big" : "Little", spi_mode, static_cast(data_rate / 1000000), + bus_width); + LOG_PIN(" CS Pin: ", cs); + LOG_PIN(" Reset Pin: ", reset); + LOG_PIN(" DC Pin: ", dc); + if (offset_width != 0) + ESP_LOGCONFIG(TAG, " Offset width: %d", offset_width); + if (offset_height != 0) + ESP_LOGCONFIG(TAG, " Offset height: %d", offset_height); + if (brightness.has_value()) + ESP_LOGCONFIG(TAG, " Brightness: %u", brightness.value()); +} + +} // namespace esphome::mipi_spi diff --git a/esphome/components/mipi_spi/mipi_spi.h b/esphome/components/mipi_spi/mipi_spi.h index fd5bc97596..083ff9507f 100644 --- a/esphome/components/mipi_spi/mipi_spi.h +++ b/esphome/components/mipi_spi/mipi_spi.h @@ -63,6 +63,11 @@ enum BusType { BUS_TYPE_SINGLE_16 = 16, // Single bit bus, but 16 bits per transfer }; +// Helper function for dump_config - defined in mipi_spi.cpp to allow use of LOG_PIN macro +void internal_dump_config(const char *model, int width, int height, int offset_width, int offset_height, uint8_t madctl, + bool invert_colors, int display_bits, bool is_big_endian, const optional &brightness, + GPIOPin *cs, GPIOPin *reset, GPIOPin *dc, int spi_mode, uint32_t data_rate, int bus_width); + /** * Base class for MIPI SPI displays. * All the methods are defined here in the header file, as it is not possible to define templated methods in a cpp file. @@ -201,37 +206,9 @@ class MipiSpi : public display::Display, } void dump_config() override { - esph_log_config(TAG, - "MIPI_SPI Display\n" - " Model: %s\n" - " Width: %u\n" - " Height: %u", - this->model_, WIDTH, HEIGHT); - if constexpr (OFFSET_WIDTH != 0) - esph_log_config(TAG, " Offset width: %u", OFFSET_WIDTH); - if constexpr (OFFSET_HEIGHT != 0) - esph_log_config(TAG, " Offset height: %u", OFFSET_HEIGHT); - esph_log_config(TAG, - " Swap X/Y: %s\n" - " Mirror X: %s\n" - " Mirror Y: %s\n" - " Invert colors: %s\n" - " Color order: %s\n" - " Display pixels: %d bits\n" - " Endianness: %s\n", - YESNO(this->madctl_ & MADCTL_MV), YESNO(this->madctl_ & (MADCTL_MX | MADCTL_XFLIP)), - YESNO(this->madctl_ & (MADCTL_MY | MADCTL_YFLIP)), YESNO(this->invert_colors_), - this->madctl_ & MADCTL_BGR ? "BGR" : "RGB", DISPLAYPIXEL * 8, IS_BIG_ENDIAN ? "Big" : "Little"); - if (this->brightness_.has_value()) - esph_log_config(TAG, " Brightness: %u", this->brightness_.value()); - log_pin(TAG, " CS Pin: ", this->cs_); - log_pin(TAG, " Reset Pin: ", this->reset_pin_); - log_pin(TAG, " DC Pin: ", this->dc_pin_); - esph_log_config(TAG, - " SPI Mode: %d\n" - " SPI Data rate: %dMHz\n" - " SPI Bus width: %d", - this->mode_, static_cast(this->data_rate_ / 1000000), BUS_TYPE); + internal_dump_config(this->model_, WIDTH, HEIGHT, OFFSET_WIDTH, OFFSET_HEIGHT, this->madctl_, this->invert_colors_, + DISPLAYPIXEL * 8, IS_BIG_ENDIAN, this->brightness_, this->cs_, this->reset_pin_, this->dc_pin_, + this->mode_, this->data_rate_, BUS_TYPE); } protected: diff --git a/esphome/components/mixer/speaker/automation.h b/esphome/components/mixer/speaker/automation.h index 2234936628..2fb2f49373 100644 --- a/esphome/components/mixer/speaker/automation.h +++ b/esphome/components/mixer/speaker/automation.h @@ -1,5 +1,6 @@ #pragma once +#include "esphome/core/automation.h" #include "mixer_speaker.h" #ifdef USE_ESP32 diff --git a/esphome/components/mlx90393/sensor_mlx90393.cpp b/esphome/components/mlx90393/sensor_mlx90393.cpp index 21a5b3a829..ee52f9b9ab 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.cpp +++ b/esphome/components/mlx90393/sensor_mlx90393.cpp @@ -132,8 +132,6 @@ void MLX90393Cls::dump_config() { LOG_SENSOR(" ", "Temperature", this->t_sensor_); } -float MLX90393Cls::get_setup_priority() const { return setup_priority::DATA; } - void MLX90393Cls::update() { MLX90393::txyz data; diff --git a/esphome/components/mlx90393/sensor_mlx90393.h b/esphome/components/mlx90393/sensor_mlx90393.h index 8a6f3321f9..845ae87e09 100644 --- a/esphome/components/mlx90393/sensor_mlx90393.h +++ b/esphome/components/mlx90393/sensor_mlx90393.h @@ -25,7 +25,6 @@ class MLX90393Cls : public PollingComponent, public i2c::I2CDevice, public MLX90 public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_gpio(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/mlx90614/mlx90614.cpp b/esphome/components/mlx90614/mlx90614.cpp index 8e53b9e3c3..8a514cbc26 100644 --- a/esphome/components/mlx90614/mlx90614.cpp +++ b/esphome/components/mlx90614/mlx90614.cpp @@ -71,8 +71,6 @@ void MLX90614Component::dump_config() { LOG_SENSOR(" ", "Object", this->object_sensor_); } -float MLX90614Component::get_setup_priority() const { return setup_priority::DATA; } - void MLX90614Component::update() { uint8_t emissivity[3]; if (this->read_register(MLX90614_EMISSIVITY, emissivity, 3) != i2c::ERROR_OK) { diff --git a/esphome/components/mlx90614/mlx90614.h b/esphome/components/mlx90614/mlx90614.h index fa6fb523bb..bf081c3e90 100644 --- a/esphome/components/mlx90614/mlx90614.h +++ b/esphome/components/mlx90614/mlx90614.h @@ -12,7 +12,6 @@ class MLX90614Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; void set_ambient_sensor(sensor::Sensor *ambient_sensor) { ambient_sensor_ = ambient_sensor; } void set_object_sensor(sensor::Sensor *object_sensor) { object_sensor_ = object_sensor; } diff --git a/esphome/components/mmc5603/mmc5603.cpp b/esphome/components/mmc5603/mmc5603.cpp index d6321eae8f..1cbc84191f 100644 --- a/esphome/components/mmc5603/mmc5603.cpp +++ b/esphome/components/mmc5603/mmc5603.cpp @@ -91,8 +91,6 @@ void MMC5603Component::dump_config() { LOG_SENSOR(" ", "Heading", this->heading_sensor_); } -float MMC5603Component::get_setup_priority() const { return setup_priority::DATA; } - void MMC5603Component::update() { uint8_t ctrl0 = (this->auto_set_reset_) ? 0x21 : 0x01; if (!this->write_byte(MMC56X3_CTRL0_REG, ctrl0)) { diff --git a/esphome/components/mmc5603/mmc5603.h b/esphome/components/mmc5603/mmc5603.h index 09718bd3b7..f827e27e04 100644 --- a/esphome/components/mmc5603/mmc5603.h +++ b/esphome/components/mmc5603/mmc5603.h @@ -17,7 +17,6 @@ class MMC5603Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_datarate(MMC5603Datarate datarate) { datarate_ = datarate; } diff --git a/esphome/components/mmc5983/mmc5983.cpp b/esphome/components/mmc5983/mmc5983.cpp index 1e0065020c..b038084a72 100644 --- a/esphome/components/mmc5983/mmc5983.cpp +++ b/esphome/components/mmc5983/mmc5983.cpp @@ -133,7 +133,5 @@ void MMC5983Component::dump_config() { LOG_SENSOR(" ", "Z", this->z_sensor_); } -float MMC5983Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mmc5983 } // namespace esphome diff --git a/esphome/components/mmc5983/mmc5983.h b/esphome/components/mmc5983/mmc5983.h index d425418904..3e87e54daa 100644 --- a/esphome/components/mmc5983/mmc5983.h +++ b/esphome/components/mmc5983/mmc5983.h @@ -12,7 +12,6 @@ class MMC5983Component : public PollingComponent, public i2c::I2CDevice { void update() override; void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_x_sensor(sensor::Sensor *x_sensor) { x_sensor_ = x_sensor; } void set_y_sensor(sensor::Sensor *y_sensor) { y_sensor_ = y_sensor; } diff --git a/esphome/components/mpu6050/mpu6050.cpp b/esphome/components/mpu6050/mpu6050.cpp index ecbee11c48..91a84d061a 100644 --- a/esphome/components/mpu6050/mpu6050.cpp +++ b/esphome/components/mpu6050/mpu6050.cpp @@ -140,7 +140,6 @@ void MPU6050Component::update() { this->status_clear_warning(); } -float MPU6050Component::get_setup_priority() const { return setup_priority::DATA; } } // namespace mpu6050 } // namespace esphome diff --git a/esphome/components/mpu6050/mpu6050.h b/esphome/components/mpu6050/mpu6050.h index ab410105c0..cc7c3620df 100644 --- a/esphome/components/mpu6050/mpu6050.h +++ b/esphome/components/mpu6050/mpu6050.h @@ -14,8 +14,6 @@ class MPU6050Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/mpu6886/mpu6886.cpp b/esphome/components/mpu6886/mpu6886.cpp index 6fdf7b8684..68b77b59c9 100644 --- a/esphome/components/mpu6886/mpu6886.cpp +++ b/esphome/components/mpu6886/mpu6886.cpp @@ -146,7 +146,5 @@ void MPU6886Component::update() { this->status_clear_warning(); } -float MPU6886Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace mpu6886 } // namespace esphome diff --git a/esphome/components/mpu6886/mpu6886.h b/esphome/components/mpu6886/mpu6886.h index 04551ae56d..96e2bf61a1 100644 --- a/esphome/components/mpu6886/mpu6886.h +++ b/esphome/components/mpu6886/mpu6886.h @@ -14,8 +14,6 @@ class MPU6886Component : public PollingComponent, public i2c::I2CDevice { void update() override; - float get_setup_priority() const override; - void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; } void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; } void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; } diff --git a/esphome/components/mqtt/__init__.py b/esphome/components/mqtt/__init__.py index f53df5564c..fe153fedfa 100644 --- a/esphome/components/mqtt/__init__.py +++ b/esphome/components/mqtt/__init__.py @@ -4,7 +4,10 @@ from esphome import automation from esphome.automation import Condition import esphome.codegen as cg from esphome.components import logger, socket -from esphome.components.esp32 import add_idf_sdkconfig_option +from esphome.components.esp32 import ( + add_idf_sdkconfig_option, + include_builtin_idf_component, +) from esphome.config_helpers import filter_source_files_from_platform import esphome.config_validation as cv from esphome.const import ( @@ -360,6 +363,8 @@ async def to_code(config): # This enables low-latency MQTT event processing instead of waiting for select() timeout if CORE.is_esp32: socket.require_wake_loop_threadsafe() + # Re-enable ESP-IDF's mqtt component (excluded by default to save compile time) + include_builtin_idf_component("mqtt") cg.add_define("USE_MQTT") cg.add_global(mqtt_ns.using) diff --git a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp index 263e554778..dc8f75d8f5 100644 --- a/esphome/components/mqtt/mqtt_alarm_control_panel.cpp +++ b/esphome/components/mqtt/mqtt_alarm_control_panel.cpp @@ -13,6 +13,33 @@ static const char *const TAG = "mqtt.alarm_control_panel"; using namespace esphome::alarm_control_panel; +static ProgmemStr alarm_state_to_mqtt_str(AlarmControlPanelState state) { + switch (state) { + case ACP_STATE_DISARMED: + return ESPHOME_F("disarmed"); + case ACP_STATE_ARMED_HOME: + return ESPHOME_F("armed_home"); + case ACP_STATE_ARMED_AWAY: + return ESPHOME_F("armed_away"); + case ACP_STATE_ARMED_NIGHT: + return ESPHOME_F("armed_night"); + case ACP_STATE_ARMED_VACATION: + return ESPHOME_F("armed_vacation"); + case ACP_STATE_ARMED_CUSTOM_BYPASS: + return ESPHOME_F("armed_custom_bypass"); + case ACP_STATE_PENDING: + return ESPHOME_F("pending"); + case ACP_STATE_ARMING: + return ESPHOME_F("arming"); + case ACP_STATE_DISARMING: + return ESPHOME_F("disarming"); + case ACP_STATE_TRIGGERED: + return ESPHOME_F("triggered"); + default: + return ESPHOME_F("unknown"); + } +} + MQTTAlarmControlPanelComponent::MQTTAlarmControlPanelComponent(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} void MQTTAlarmControlPanelComponent::setup() { @@ -85,43 +112,9 @@ const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return th bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); } bool MQTTAlarmControlPanelComponent::publish_state() { - const char *state_s; - switch (this->alarm_control_panel_->get_state()) { - case ACP_STATE_DISARMED: - state_s = "disarmed"; - break; - case ACP_STATE_ARMED_HOME: - state_s = "armed_home"; - break; - case ACP_STATE_ARMED_AWAY: - state_s = "armed_away"; - break; - case ACP_STATE_ARMED_NIGHT: - state_s = "armed_night"; - break; - case ACP_STATE_ARMED_VACATION: - state_s = "armed_vacation"; - break; - case ACP_STATE_ARMED_CUSTOM_BYPASS: - state_s = "armed_custom_bypass"; - break; - case ACP_STATE_PENDING: - state_s = "pending"; - break; - case ACP_STATE_ARMING: - state_s = "arming"; - break; - case ACP_STATE_DISARMING: - state_s = "disarming"; - break; - case ACP_STATE_TRIGGERED: - state_s = "triggered"; - break; - default: - state_s = "unknown"; - } char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - return this->publish(this->get_state_topic_to_(topic_buf), state_s); + return this->publish(this->get_state_topic_to_(topic_buf), + alarm_state_to_mqtt_str(this->alarm_control_panel_->get_state())); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_backend_esp32.h b/esphome/components/mqtt/mqtt_backend_esp32.h index bd2d2a67b2..adba0cf004 100644 --- a/esphome/components/mqtt/mqtt_backend_esp32.h +++ b/esphome/components/mqtt/mqtt_backend_esp32.h @@ -139,7 +139,8 @@ class MQTTBackendESP32 final : public MQTTBackend { this->lwt_retain_ = retain; } void set_server(network::IPAddress ip, uint16_t port) final { - this->host_ = ip.str(); + char ip_buf[network::IP_ADDRESS_BUFFER_SIZE]; + this->host_ = ip.str_to(ip_buf); this->port_ = port; } void set_server(const char *host, uint16_t port) final { diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index e7364f3406..a284b162dd 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -643,10 +643,34 @@ static bool topic_match(const char *message, const char *subscription) { } void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) { - for (auto &subscription : this->subscriptions_) { - if (topic_match(topic.c_str(), subscription.topic.c_str())) - subscription.callback(topic, payload); - } +#ifdef USE_ESP8266 + // IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266. + // + // On ESP8266, this callback is invoked directly from the lwIP/AsyncTCP network stack + // which runs in the "sys" context with a very limited stack (~4KB). By the time we + // reach this function, the stack is already partially consumed by the network + // processing chain: tcp_input -> AsyncClient::_recv -> AsyncMqttClient::_onMessage -> here. + // + // MQTT subscription callbacks can trigger arbitrary user actions (automations, HTTP + // requests, sensor updates, etc.) which may have deep call stacks of their own. + // For example, an HTTP request action requires: DNS lookup -> TCP connect -> TLS + // handshake (if HTTPS) -> request formatting. This easily overflows the remaining + // system stack space, causing a LoadStoreAlignmentCause exception or silent corruption. + // + // By deferring to the main loop, we ensure callbacks execute with a fresh, full-size + // stack in the normal application context rather than the constrained network task. + // + // DO NOT REMOVE THIS DEFER without understanding the above. It may appear to work + // in simple tests but will cause crashes with complex automations. + this->defer([this, topic, payload]() { +#endif + for (auto &subscription : this->subscriptions_) { + if (topic_match(topic.c_str(), subscription.topic.c_str())) + subscription.callback(topic, payload); + } +#ifdef USE_ESP8266 + }); +#endif } // Setters diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 37d643f9e7..673593ef84 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -1,5 +1,6 @@ #include "mqtt_climate.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,111 @@ static const char *const TAG = "mqtt.climate"; using namespace esphome::climate; +static ProgmemStr climate_mode_to_mqtt_str(ClimateMode mode) { + switch (mode) { + case CLIMATE_MODE_OFF: + return ESPHOME_F("off"); + case CLIMATE_MODE_HEAT_COOL: + return ESPHOME_F("heat_cool"); + case CLIMATE_MODE_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_MODE_COOL: + return ESPHOME_F("cool"); + case CLIMATE_MODE_HEAT: + return ESPHOME_F("heat"); + case CLIMATE_MODE_FAN_ONLY: + return ESPHOME_F("fan_only"); + case CLIMATE_MODE_DRY: + return ESPHOME_F("dry"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_action_to_mqtt_str(ClimateAction action) { + switch (action) { + case CLIMATE_ACTION_OFF: + return ESPHOME_F("off"); + case CLIMATE_ACTION_COOLING: + return ESPHOME_F("cooling"); + case CLIMATE_ACTION_HEATING: + return ESPHOME_F("heating"); + case CLIMATE_ACTION_IDLE: + return ESPHOME_F("idle"); + case CLIMATE_ACTION_DRYING: + return ESPHOME_F("drying"); + case CLIMATE_ACTION_FAN: + return ESPHOME_F("fan"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_fan_mode_to_mqtt_str(ClimateFanMode fan_mode) { + switch (fan_mode) { + case CLIMATE_FAN_ON: + return ESPHOME_F("on"); + case CLIMATE_FAN_OFF: + return ESPHOME_F("off"); + case CLIMATE_FAN_AUTO: + return ESPHOME_F("auto"); + case CLIMATE_FAN_LOW: + return ESPHOME_F("low"); + case CLIMATE_FAN_MEDIUM: + return ESPHOME_F("medium"); + case CLIMATE_FAN_HIGH: + return ESPHOME_F("high"); + case CLIMATE_FAN_MIDDLE: + return ESPHOME_F("middle"); + case CLIMATE_FAN_FOCUS: + return ESPHOME_F("focus"); + case CLIMATE_FAN_DIFFUSE: + return ESPHOME_F("diffuse"); + case CLIMATE_FAN_QUIET: + return ESPHOME_F("quiet"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_swing_mode_to_mqtt_str(ClimateSwingMode swing_mode) { + switch (swing_mode) { + case CLIMATE_SWING_OFF: + return ESPHOME_F("off"); + case CLIMATE_SWING_BOTH: + return ESPHOME_F("both"); + case CLIMATE_SWING_VERTICAL: + return ESPHOME_F("vertical"); + case CLIMATE_SWING_HORIZONTAL: + return ESPHOME_F("horizontal"); + default: + return ESPHOME_F("unknown"); + } +} + +static ProgmemStr climate_preset_to_mqtt_str(ClimatePreset preset) { + switch (preset) { + case CLIMATE_PRESET_NONE: + return ESPHOME_F("none"); + case CLIMATE_PRESET_HOME: + return ESPHOME_F("home"); + case CLIMATE_PRESET_ECO: + return ESPHOME_F("eco"); + case CLIMATE_PRESET_AWAY: + return ESPHOME_F("away"); + case CLIMATE_PRESET_BOOST: + return ESPHOME_F("boost"); + case CLIMATE_PRESET_COMFORT: + return ESPHOME_F("comfort"); + case CLIMATE_PRESET_SLEEP: + return ESPHOME_F("sleep"); + case CLIMATE_PRESET_ACTIVITY: + return ESPHOME_F("activity"); + default: + return ESPHOME_F("unknown"); + } +} + void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson auto traits = this->device_->get_traits(); @@ -260,34 +366,8 @@ const EntityBase *MQTTClimateComponent::get_entity() const { return this->device bool MQTTClimateComponent::publish_state_() { auto traits = this->device_->get_traits(); // mode - const char *mode_s; - switch (this->device_->mode) { - case CLIMATE_MODE_OFF: - mode_s = "off"; - break; - case CLIMATE_MODE_AUTO: - mode_s = "auto"; - break; - case CLIMATE_MODE_COOL: - mode_s = "cool"; - break; - case CLIMATE_MODE_HEAT: - mode_s = "heat"; - break; - case CLIMATE_MODE_FAN_ONLY: - mode_s = "fan_only"; - break; - case CLIMATE_MODE_DRY: - mode_s = "dry"; - break; - case CLIMATE_MODE_HEAT_COOL: - mode_s = "heat_cool"; - break; - default: - mode_s = "unknown"; - } bool success = true; - if (!this->publish(this->get_mode_state_topic(), mode_s)) + if (!this->publish(this->get_mode_state_topic(), climate_mode_to_mqtt_str(this->device_->mode))) success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); @@ -327,134 +407,37 @@ bool MQTTClimateComponent::publish_state_() { } if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { - std::string payload; - if (this->device_->preset.has_value()) { - switch (this->device_->preset.value()) { - case CLIMATE_PRESET_NONE: - payload = "none"; - break; - case CLIMATE_PRESET_HOME: - payload = "home"; - break; - case CLIMATE_PRESET_AWAY: - payload = "away"; - break; - case CLIMATE_PRESET_BOOST: - payload = "boost"; - break; - case CLIMATE_PRESET_COMFORT: - payload = "comfort"; - break; - case CLIMATE_PRESET_ECO: - payload = "eco"; - break; - case CLIMATE_PRESET_SLEEP: - payload = "sleep"; - break; - case CLIMATE_PRESET_ACTIVITY: - payload = "activity"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_preset()) - payload = this->device_->get_custom_preset().c_str(); - if (!this->publish(this->get_preset_state_topic(), payload)) + if (this->device_->has_custom_preset()) { + if (!this->publish(this->get_preset_state_topic(), this->device_->get_custom_preset())) + success = false; + } else if (this->device_->preset.has_value()) { + if (!this->publish(this->get_preset_state_topic(), climate_preset_to_mqtt_str(this->device_->preset.value()))) + success = false; + } else if (!this->publish(this->get_preset_state_topic(), "")) { success = false; + } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { - const char *payload; - switch (this->device_->action) { - case CLIMATE_ACTION_OFF: - payload = "off"; - break; - case CLIMATE_ACTION_COOLING: - payload = "cooling"; - break; - case CLIMATE_ACTION_HEATING: - payload = "heating"; - break; - case CLIMATE_ACTION_IDLE: - payload = "idle"; - break; - case CLIMATE_ACTION_DRYING: - payload = "drying"; - break; - case CLIMATE_ACTION_FAN: - payload = "fan"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_action_state_topic(), payload)) + if (!this->publish(this->get_action_state_topic(), climate_action_to_mqtt_str(this->device_->action))) success = false; } if (traits.get_supports_fan_modes()) { - std::string payload; - if (this->device_->fan_mode.has_value()) { - switch (this->device_->fan_mode.value()) { - case CLIMATE_FAN_ON: - payload = "on"; - break; - case CLIMATE_FAN_OFF: - payload = "off"; - break; - case CLIMATE_FAN_AUTO: - payload = "auto"; - break; - case CLIMATE_FAN_LOW: - payload = "low"; - break; - case CLIMATE_FAN_MEDIUM: - payload = "medium"; - break; - case CLIMATE_FAN_HIGH: - payload = "high"; - break; - case CLIMATE_FAN_MIDDLE: - payload = "middle"; - break; - case CLIMATE_FAN_FOCUS: - payload = "focus"; - break; - case CLIMATE_FAN_DIFFUSE: - payload = "diffuse"; - break; - case CLIMATE_FAN_QUIET: - payload = "quiet"; - break; - default: - payload = "unknown"; - } - } - if (this->device_->has_custom_fan_mode()) - payload = this->device_->get_custom_fan_mode().c_str(); - if (!this->publish(this->get_fan_mode_state_topic(), payload)) + if (this->device_->has_custom_fan_mode()) { + if (!this->publish(this->get_fan_mode_state_topic(), this->device_->get_custom_fan_mode())) + success = false; + } else if (this->device_->fan_mode.has_value()) { + if (!this->publish(this->get_fan_mode_state_topic(), + climate_fan_mode_to_mqtt_str(this->device_->fan_mode.value()))) + success = false; + } else if (!this->publish(this->get_fan_mode_state_topic(), "")) { success = false; + } } if (traits.get_supports_swing_modes()) { - const char *payload; - switch (this->device_->swing_mode) { - case CLIMATE_SWING_OFF: - payload = "off"; - break; - case CLIMATE_SWING_BOTH: - payload = "both"; - break; - case CLIMATE_SWING_VERTICAL: - payload = "vertical"; - break; - case CLIMATE_SWING_HORIZONTAL: - payload = "horizontal"; - break; - default: - payload = "unknown"; - } - if (!this->publish(this->get_swing_mode_state_topic(), payload)) + if (!this->publish(this->get_swing_mode_state_topic(), climate_swing_mode_to_mqtt_str(this->device_->swing_mode))) success = false; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index aec6140e3f..a77afd3f4e 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -5,6 +5,7 @@ #include "esphome/core/application.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/version.h" #include "mqtt_const.h" @@ -149,6 +150,22 @@ bool MQTTComponent::publish(const char *topic, const char *payload) { return this->publish(topic, payload, strlen(payload)); } +#ifdef USE_ESP8266 +bool MQTTComponent::publish(const std::string &topic, ProgmemStr payload) { + return this->publish(topic.c_str(), payload); +} + +bool MQTTComponent::publish(const char *topic, ProgmemStr payload) { + if (topic[0] == '\0') + return false; + // On ESP8266, ProgmemStr is __FlashStringHelper* - need to copy from flash + char buf[64]; + strncpy_P(buf, reinterpret_cast(payload), sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + return global_mqtt_client->publish(topic, buf, strlen(buf), this->qos_, this->retain_); +} +#endif + bool MQTTComponent::publish_json(const std::string &topic, const json::json_build_t &f) { return this->publish_json(topic.c_str(), f); } diff --git a/esphome/components/mqtt/mqtt_component.h b/esphome/components/mqtt/mqtt_component.h index 304a2c0d0e..2cec6fda7e 100644 --- a/esphome/components/mqtt/mqtt_component.h +++ b/esphome/components/mqtt/mqtt_component.h @@ -9,6 +9,7 @@ #include "esphome/core/automation.h" #include "esphome/core/component.h" #include "esphome/core/entity_base.h" +#include "esphome/core/progmem.h" #include "esphome/core/string_ref.h" #include "mqtt_client.h" @@ -157,6 +158,15 @@ class MQTTComponent : public Component { */ bool publish(const std::string &topic, const char *payload, size_t payload_length); + /** Send a MQTT message. + * + * @param topic The topic. + * @param payload The null-terminated payload. + */ + bool publish(const std::string &topic, const char *payload) { + return this->publish(topic.c_str(), payload, strlen(payload)); + } + /** Send a MQTT message (no heap allocation for topic). * * @param topic The topic as C string. @@ -189,6 +199,29 @@ class MQTTComponent : public Component { */ bool publish(StringRef topic, const char *payload) { return this->publish(topic.c_str(), payload); } +#ifdef USE_ESP8266 + /** Send a MQTT message with a PROGMEM string payload. + * + * @param topic The topic. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const std::string &topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as C string. + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(const char *topic, ProgmemStr payload); + + /** Send a MQTT message with a PROGMEM string payload (no heap allocation for topic). + * + * @param topic The topic as StringRef (for use with get_state_topic_to_()). + * @param payload The payload (ProgmemStr - stored in flash on ESP8266). + */ + bool publish(StringRef topic, ProgmemStr payload) { return this->publish(topic.c_str(), payload); } +#endif + /** Construct and send a JSON MQTT message. * * @param topic The topic. diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index d5bd13869a..50e68ecbcc 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -1,5 +1,6 @@ #include "mqtt_cover.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.cover"; using namespace esphome::cover; +static ProgmemStr cover_state_to_mqtt_str(CoverOperation operation, float position, bool supports_position) { + if (operation == COVER_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == COVER_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == COVER_CLOSED) + return ESPHOME_F("closed"); + if (position == COVER_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTCoverComponent::MQTTCoverComponent(Cover *cover) : cover_(cover) {} void MQTTCoverComponent::setup() { auto traits = this->cover_->get_traits(); @@ -109,14 +124,10 @@ bool MQTTCoverComponent::publish_state() { if (!this->publish(this->get_tilt_state_topic(), pos, len)) success = false; } - const char *state_s = this->cover_->current_operation == COVER_OPERATION_OPENING ? "opening" - : this->cover_->current_operation == COVER_OPERATION_CLOSING ? "closing" - : this->cover_->position == COVER_CLOSED ? "closed" - : this->cover_->position == COVER_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + cover_state_to_mqtt_str(this->cover_->current_operation, this->cover_->position, + traits.get_supports_position()))) success = false; return success; } diff --git a/esphome/components/mqtt/mqtt_fan.cpp b/esphome/components/mqtt/mqtt_fan.cpp index c9791fb0f1..84d51895c5 100644 --- a/esphome/components/mqtt/mqtt_fan.cpp +++ b/esphome/components/mqtt/mqtt_fan.cpp @@ -1,5 +1,6 @@ #include "mqtt_fan.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,14 @@ static const char *const TAG = "mqtt.fan"; using namespace esphome::fan; +static ProgmemStr fan_direction_to_mqtt_str(FanDirection direction) { + return direction == FanDirection::FORWARD ? ESPHOME_F("forward") : ESPHOME_F("reverse"); +} + +static ProgmemStr fan_oscillation_to_mqtt_str(bool oscillating) { + return oscillating ? ESPHOME_F("oscillate_on") : ESPHOME_F("oscillate_off"); +} + MQTTFanComponent::MQTTFanComponent(Fan *state) : state_(state) {} Fan *MQTTFanComponent::get_state() const { return this->state_; } @@ -164,13 +173,12 @@ bool MQTTFanComponent::publish_state() { this->publish(this->get_state_topic_to_(topic_buf), state_s); bool failed = false; if (this->state_->get_traits().supports_direction()) { - bool success = this->publish(this->get_direction_state_topic(), - this->state_->direction == fan::FanDirection::FORWARD ? "forward" : "reverse"); + bool success = this->publish(this->get_direction_state_topic(), fan_direction_to_mqtt_str(this->state_->direction)); failed = failed || !success; } if (this->state_->get_traits().supports_oscillation()) { - bool success = this->publish(this->get_oscillation_state_topic(), - this->state_->oscillating ? "oscillate_on" : "oscillate_off"); + bool success = + this->publish(this->get_oscillation_state_topic(), fan_oscillation_to_mqtt_str(this->state_->oscillating)); failed = failed || !success; } auto traits = this->state_->get_traits(); diff --git a/esphome/components/mqtt/mqtt_light.cpp b/esphome/components/mqtt/mqtt_light.cpp index 3e3537fa5c..aa47bdf996 100644 --- a/esphome/components/mqtt/mqtt_light.cpp +++ b/esphome/components/mqtt/mqtt_light.cpp @@ -47,7 +47,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery root[ESPHOME_F("schema")] = ESPHOME_F("json"); auto traits = this->state_->get_traits(); - root[MQTT_COLOR_MODE] = true; // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson JsonArray color_modes = root[ESPHOME_F("supported_color_modes")].to(); if (traits.supports_color_mode(ColorMode::ON_OFF)) @@ -68,10 +67,6 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery if (traits.supports_color_mode(ColorMode::RGB_COLD_WARM_WHITE)) color_modes.add(ESPHOME_F("rgbww")); - // legacy API - if (traits.supports_color_capability(ColorCapability::BRIGHTNESS)) - root[ESPHOME_F("brightness")] = true; - if (traits.supports_color_mode(ColorMode::COLOR_TEMPERATURE) || traits.supports_color_mode(ColorMode::COLD_WARM_WHITE)) { root[MQTT_MIN_MIREDS] = traits.get_min_mireds(); diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index 2e100823bf..16e25f6a8a 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -1,5 +1,6 @@ #include "mqtt_valve.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "mqtt_const.h" @@ -12,6 +13,20 @@ static const char *const TAG = "mqtt.valve"; using namespace esphome::valve; +static ProgmemStr valve_state_to_mqtt_str(ValveOperation operation, float position, bool supports_position) { + if (operation == VALVE_OPERATION_OPENING) + return ESPHOME_F("opening"); + if (operation == VALVE_OPERATION_CLOSING) + return ESPHOME_F("closing"); + if (position == VALVE_CLOSED) + return ESPHOME_F("closed"); + if (position == VALVE_OPEN) + return ESPHOME_F("open"); + if (supports_position) + return ESPHOME_F("open"); + return ESPHOME_F("unknown"); +} + MQTTValveComponent::MQTTValveComponent(Valve *valve) : valve_(valve) {} void MQTTValveComponent::setup() { auto traits = this->valve_->get_traits(); @@ -78,14 +93,10 @@ bool MQTTValveComponent::publish_state() { if (!this->publish(this->get_position_state_topic(), pos, len)) success = false; } - const char *state_s = this->valve_->current_operation == VALVE_OPERATION_OPENING ? "opening" - : this->valve_->current_operation == VALVE_OPERATION_CLOSING ? "closing" - : this->valve_->position == VALVE_CLOSED ? "closed" - : this->valve_->position == VALVE_OPEN ? "open" - : traits.get_supports_position() ? "open" - : "unknown"; char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN]; - if (!this->publish(this->get_state_topic_to_(topic_buf), state_s)) + if (!this->publish(this->get_state_topic_to_(topic_buf), + valve_state_to_mqtt_str(this->valve_->current_operation, this->valve_->position, + traits.get_supports_position()))) success = false; return success; } diff --git a/esphome/components/ms5611/ms5611.cpp b/esphome/components/ms5611/ms5611.cpp index 5a7622e783..7ed73400c8 100644 --- a/esphome/components/ms5611/ms5611.cpp +++ b/esphome/components/ms5611/ms5611.cpp @@ -38,7 +38,6 @@ void MS5611Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Pressure", this->pressure_sensor_); } -float MS5611Component::get_setup_priority() const { return setup_priority::DATA; } void MS5611Component::update() { // request temperature reading if (!this->write_bytes(MS5611_CMD_CONV_D2 + 0x08, nullptr, 0)) { diff --git a/esphome/components/ms5611/ms5611.h b/esphome/components/ms5611/ms5611.h index 476db79612..7e4806f319 100644 --- a/esphome/components/ms5611/ms5611.h +++ b/esphome/components/ms5611/ms5611.h @@ -11,7 +11,6 @@ class MS5611Component : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index 56dc919968..e46bfed193 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -287,7 +287,6 @@ void MSA3xxComponent::update() { this->status_.never_published = false; this->status_clear_warning(); } -float MSA3xxComponent::get_setup_priority() const { return setup_priority::DATA; } void MSA3xxComponent::set_offset(float offset_x, float offset_y, float offset_z) { this->offset_x_ = offset_x; diff --git a/esphome/components/msa3xx/msa3xx.h b/esphome/components/msa3xx/msa3xx.h index 644109dab0..439d3b5f4d 100644 --- a/esphome/components/msa3xx/msa3xx.h +++ b/esphome/components/msa3xx/msa3xx.h @@ -220,8 +220,6 @@ class MSA3xxComponent : public PollingComponent, public i2c::I2CDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_model(Model model) { this->model_ = model; } void set_offset(float offset_x, float offset_y, float offset_z); void set_range(Range range) { this->range_ = range; } diff --git a/esphome/components/nau7802/nau7802.cpp b/esphome/components/nau7802/nau7802.cpp index 5edbc79862..937239b98d 100644 --- a/esphome/components/nau7802/nau7802.cpp +++ b/esphome/components/nau7802/nau7802.cpp @@ -296,8 +296,6 @@ void NAU7802Sensor::loop() { } } -float NAU7802Sensor::get_setup_priority() const { return setup_priority::DATA; } - void NAU7802Sensor::update() { if (!this->is_data_ready_()) { ESP_LOGW(TAG, "No measurements ready!"); diff --git a/esphome/components/nau7802/nau7802.h b/esphome/components/nau7802/nau7802.h index 05452851ca..ae39e167a4 100644 --- a/esphome/components/nau7802/nau7802.h +++ b/esphome/components/nau7802/nau7802.h @@ -62,7 +62,6 @@ class NAU7802Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/neopixelbus/light.py b/esphome/components/neopixelbus/light.py index c77217243c..104762c69e 100644 --- a/esphome/components/neopixelbus/light.py +++ b/esphome/components/neopixelbus/light.py @@ -1,7 +1,12 @@ from esphome import pins import esphome.codegen as cg from esphome.components import light -from esphome.components.esp32 import VARIANT_ESP32C3, VARIANT_ESP32S3, get_esp32_variant +from esphome.components.esp32 import ( + VARIANT_ESP32C3, + VARIANT_ESP32S3, + get_esp32_variant, + include_builtin_idf_component, +) import esphome.config_validation as cv from esphome.const import ( CONF_CHANNEL, @@ -205,6 +210,10 @@ async def to_code(config): has_white = "W" in config[CONF_TYPE] method = config[CONF_METHOD] + # Re-enable ESP-IDF's RMT driver if using RMT method (excluded by default) + if CORE.is_esp32 and method[CONF_TYPE] == METHOD_ESP32_RMT: + include_builtin_idf_component("esp_driver_rmt") + method_template = METHODS[method[CONF_TYPE]].to_code( method, config[CONF_VARIANT], config[CONF_INVERT] ) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 5b63bbfce9..1f75b12178 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -137,8 +137,7 @@ CONFIG_SCHEMA = cv.Schema( @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") - if CORE.using_arduino and CORE.is_esp32: - cg.add_library("Networking", None) + # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index ffc509fc64..3bfcc95995 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -177,6 +177,8 @@ async def to_code(config): cg.add_define("USE_NEXTION_TFT_UPLOAD") cg.add(var.set_tft_url(config[CONF_TFT_URL])) if CORE.is_esp32: + # Re-enable ESP-IDF's HTTP client (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_http_client") esp32.add_idf_sdkconfig_option("CONFIG_ESP_TLS_INSECURE", True) esp32.add_idf_sdkconfig_option( "CONFIG_ESP_TLS_SKIP_SERVER_CERT_VERIFY", True diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index e57a258edb..fd6ce0a24b 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -193,7 +193,6 @@ void Nextion::dump_config() { #endif } -float Nextion::get_setup_priority() const { return setup_priority::DATA; } void Nextion::update() { if (!this->is_setup()) { return; diff --git a/esphome/components/nextion/nextion.h b/esphome/components/nextion/nextion.h index c543e14bfe..c42ddba9b5 100644 --- a/esphome/components/nextion/nextion.h +++ b/esphome/components/nextion/nextion.h @@ -1048,7 +1048,6 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe void setup() override; void set_brightness(float brightness) { this->brightness_ = brightness; } - float get_setup_priority() const override; void update() override; void loop() override; void set_writer(const nextion_writer_t &writer); diff --git a/esphome/components/npi19/npi19.cpp b/esphome/components/npi19/npi19.cpp index c531d2ec8f..995abdff37 100644 --- a/esphome/components/npi19/npi19.cpp +++ b/esphome/components/npi19/npi19.cpp @@ -29,8 +29,6 @@ void NPI19Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float NPI19Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode NPI19Component::read_(uint16_t &raw_temperature, uint16_t &raw_pressure) { // initiate data read from device i2c::ErrorCode w_err = write(&READ_COMMAND, sizeof(READ_COMMAND)); diff --git a/esphome/components/npi19/npi19.h b/esphome/components/npi19/npi19.h index df289dffc1..8e6a8e3bfa 100644 --- a/esphome/components/npi19/npi19.h +++ b/esphome/components/npi19/npi19.h @@ -15,7 +15,6 @@ class NPI19Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/ntc/ntc.cpp b/esphome/components/ntc/ntc.cpp index cc500ba429..e2097bdd77 100644 --- a/esphome/components/ntc/ntc.cpp +++ b/esphome/components/ntc/ntc.cpp @@ -12,7 +12,6 @@ void NTC::setup() { this->process_(this->sensor_->state); } void NTC::dump_config() { LOG_SENSOR("", "NTC Sensor", this); } -float NTC::get_setup_priority() const { return setup_priority::DATA; } void NTC::process_(float value) { if (std::isnan(value)) { this->publish_state(NAN); diff --git a/esphome/components/ntc/ntc.h b/esphome/components/ntc/ntc.h index c8592e0fe8..a0c72340de 100644 --- a/esphome/components/ntc/ntc.h +++ b/esphome/components/ntc/ntc.h @@ -14,7 +14,6 @@ class NTC : public Component, public sensor::Sensor { void set_c(double c) { c_ = c; } void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: void process_(float value); diff --git a/esphome/components/number/__init__.py b/esphome/components/number/__init__.py index 368b431d7b..b23da7799f 100644 --- a/esphome/components/number/__init__.py +++ b/esphome/components/number/__init__.py @@ -1,6 +1,6 @@ from esphome import automation import esphome.codegen as cg -from esphome.components import mqtt, web_server +from esphome.components import mqtt, web_server, zigbee import esphome.config_validation as cv from esphome.const import ( CONF_ABOVE, @@ -189,6 +189,7 @@ validate_unit_of_measurement = cv.string_strict _NUMBER_SCHEMA = ( cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) + .extend(zigbee.NUMBER_SCHEMA) .extend( { cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTNumberComponent), @@ -214,6 +215,7 @@ _NUMBER_SCHEMA = ( _NUMBER_SCHEMA.add_extra(entity_duplicate_validator("number")) +_NUMBER_SCHEMA.add_extra(zigbee.validate_number) def number_schema( @@ -277,6 +279,8 @@ async def setup_number_core_( if web_server_config := config.get(CONF_WEB_SERVER): await web_server.add_entity_config(var, web_server_config) + await zigbee.setup_number(var, config, min_value, max_value, step) + async def register_number( var, config, *, min_value: float, max_value: float, step: float diff --git a/esphome/components/opentherm/__init__.py b/esphome/components/opentherm/__init__.py index 8cbee1eed2..dddb9dc891 100644 --- a/esphome/components/opentherm/__init__.py +++ b/esphome/components/opentherm/__init__.py @@ -4,8 +4,10 @@ from typing import Any from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import CONF_ID, CONF_TRIGGER_ID, PLATFORM_ESP32, PLATFORM_ESP8266 +from esphome.core import CORE from . import const, generate, schema, validate @@ -83,6 +85,12 @@ CONFIG_SCHEMA = cv.All( async def to_code(config: dict[str, Any]) -> None: + if CORE.is_esp32: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/timer.h header for hardware timer API + # TODO: Remove this once opentherm migrates to GPTimer API (driver/gptimer.h) + include_builtin_idf_component("driver") + var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) diff --git a/esphome/components/opentherm/generate.py b/esphome/components/opentherm/generate.py index 4e6f3b0a12..0b39895798 100644 --- a/esphome/components/opentherm/generate.py +++ b/esphome/components/opentherm/generate.py @@ -31,7 +31,9 @@ def define_has_settings(keys: list[str], schemas: dict[str, SettingSchema]) -> N cg.RawExpression( " sep ".join( map( - lambda key: f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})", + lambda key: ( + f"F({schemas[key].backing_type}, {key}_setting, {schemas[key].default_value})" + ), keys, ) ) diff --git a/esphome/components/opentherm/opentherm.cpp b/esphome/components/opentherm/opentherm.cpp index 2bf438a52f..cdf89207bc 100644 --- a/esphome/components/opentherm/opentherm.cpp +++ b/esphome/components/opentherm/opentherm.cpp @@ -8,6 +8,8 @@ #include "opentherm.h" #include "esphome/core/helpers.h" #include +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. See opentherm.h for details. #ifdef USE_ESP32 #include "driver/timer.h" #include "esp_err.h" diff --git a/esphome/components/opentherm/opentherm.h b/esphome/components/opentherm/opentherm.h index 3996481760..a2c347d0d8 100644 --- a/esphome/components/opentherm/opentherm.h +++ b/esphome/components/opentherm/opentherm.h @@ -12,6 +12,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// TODO: Migrate from legacy timer API (driver/timer.h) to GPTimer API (driver/gptimer.h) +// The legacy timer API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id4 #ifdef USE_ESP32 #include "driver/timer.h" #endif diff --git a/esphome/components/openthread/__init__.py b/esphome/components/openthread/__init__.py index 26c05a0a86..89d335c574 100644 --- a/esphome/components/openthread/__init__.py +++ b/esphome/components/openthread/__init__.py @@ -4,6 +4,7 @@ from esphome.components.esp32 import ( VARIANT_ESP32C6, VARIANT_ESP32H2, add_idf_sdkconfig_option, + include_builtin_idf_component, only_on_variant, require_vfs_select, ) @@ -172,6 +173,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate async def to_code(config): + # Re-enable openthread IDF component (excluded by default) + include_builtin_idf_component("openthread") + cg.add_define("USE_OPENTHREAD") # OpenThread SRP needs access to mDNS services after setup diff --git a/esphome/components/opt3001/opt3001.cpp b/esphome/components/opt3001/opt3001.cpp index f5f7ab9412..a942e45035 100644 --- a/esphome/components/opt3001/opt3001.cpp +++ b/esphome/components/opt3001/opt3001.cpp @@ -116,7 +116,5 @@ void OPT3001Sensor::update() { }); } -float OPT3001Sensor::get_setup_priority() const { return setup_priority::DATA; } - } // namespace opt3001 } // namespace esphome diff --git a/esphome/components/opt3001/opt3001.h b/esphome/components/opt3001/opt3001.h index ae3fde5c54..3bce9f0aeb 100644 --- a/esphome/components/opt3001/opt3001.h +++ b/esphome/components/opt3001/opt3001.h @@ -12,7 +12,6 @@ class OPT3001Sensor : public sensor::Sensor, public PollingComponent, public i2c public: void dump_config() override; void update() override; - float get_setup_priority() const override; protected: // checks if one-shot is complete before reading the result and returning it diff --git a/esphome/components/pcf85063/pcf85063.cpp b/esphome/components/pcf85063/pcf85063.cpp index f38b60b55d..03ed78654f 100644 --- a/esphome/components/pcf85063/pcf85063.cpp +++ b/esphome/components/pcf85063/pcf85063.cpp @@ -26,8 +26,6 @@ void PCF85063Component::dump_config() { RealTimeClock::dump_config(); } -float PCF85063Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF85063Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf85063/pcf85063.h b/esphome/components/pcf85063/pcf85063.h index b7034d4f00..697837f223 100644 --- a/esphome/components/pcf85063/pcf85063.h +++ b/esphome/components/pcf85063/pcf85063.h @@ -12,7 +12,6 @@ class PCF85063Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pcf8563/pcf8563.cpp b/esphome/components/pcf8563/pcf8563.cpp index 2090936bb6..dc68807aef 100644 --- a/esphome/components/pcf8563/pcf8563.cpp +++ b/esphome/components/pcf8563/pcf8563.cpp @@ -26,8 +26,6 @@ void PCF8563Component::dump_config() { RealTimeClock::dump_config(); } -float PCF8563Component::get_setup_priority() const { return setup_priority::DATA; } - void PCF8563Component::read_time() { if (!this->read_rtc_()) { return; diff --git a/esphome/components/pcf8563/pcf8563.h b/esphome/components/pcf8563/pcf8563.h index 81aa816b42..cd37d05816 100644 --- a/esphome/components/pcf8563/pcf8563.h +++ b/esphome/components/pcf8563/pcf8563.h @@ -12,7 +12,6 @@ class PCF8563Component : public time::RealTimeClock, public i2c::I2CDevice { void setup() override; void update() override; void dump_config() override; - float get_setup_priority() const override; void read_time(); void write_time(); diff --git a/esphome/components/pipsolar/sensor/__init__.py b/esphome/components/pipsolar/sensor/__init__.py index d08a877b55..8d3ba10d62 100644 --- a/esphome/components/pipsolar/sensor/__init__.py +++ b/esphome/components/pipsolar/sensor/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import sensor +from esphome.components.const import ICON_CURRENT_DC, ICON_SOLAR_PANEL, ICON_SOLAR_POWER import esphome.config_validation as cv from esphome.const import ( CONF_BATTERY_VOLTAGE, @@ -29,9 +30,6 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA DEPENDENCIES = ["uart"] -ICON_SOLAR_POWER = "mdi:solar-power" -ICON_SOLAR_PANEL = "mdi:solar-panel" -ICON_CURRENT_DC = "mdi:current-dc" # QPIRI sensors CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" diff --git a/esphome/components/pm1006/pm1006.cpp b/esphome/components/pm1006/pm1006.cpp index c466c4bb25..fe8890e777 100644 --- a/esphome/components/pm1006/pm1006.cpp +++ b/esphome/components/pm1006/pm1006.cpp @@ -44,8 +44,6 @@ void PM1006Component::loop() { } } -float PM1006Component::get_setup_priority() const { return setup_priority::DATA; } - uint8_t PM1006Component::pm1006_checksum_(const uint8_t *command_data, uint8_t length) const { uint8_t sum = 0; for (uint8_t i = 0; i < length; i++) { diff --git a/esphome/components/pm1006/pm1006.h b/esphome/components/pm1006/pm1006.h index 98637dad71..6b6332e1e3 100644 --- a/esphome/components/pm1006/pm1006.h +++ b/esphome/components/pm1006/pm1006.h @@ -18,8 +18,6 @@ class PM1006Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - protected: optional check_byte_() const; void parse_data_(); diff --git a/esphome/components/pm2005/pm2005.h b/esphome/components/pm2005/pm2005.h index 219fbae5cb..e788569b7e 100644 --- a/esphome/components/pm2005/pm2005.h +++ b/esphome/components/pm2005/pm2005.h @@ -14,8 +14,6 @@ enum SensorType { class PM2005Component : public PollingComponent, public i2c::I2CDevice { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } - void set_sensor_type(SensorType sensor_type) { this->sensor_type_ = sensor_type; } void set_pm_1_0_sensor(sensor::Sensor *pm_1_0_sensor) { this->pm_1_0_sensor_ = pm_1_0_sensor; } diff --git a/esphome/components/pmsx003/pmsx003.cpp b/esphome/components/pmsx003/pmsx003.cpp index bb167033d1..114ecf435e 100644 --- a/esphome/components/pmsx003/pmsx003.cpp +++ b/esphome/components/pmsx003/pmsx003.cpp @@ -2,21 +2,20 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" -namespace esphome { -namespace pmsx003 { +namespace esphome::pmsx003 { static const char *const TAG = "pmsx003"; static const uint8_t START_CHARACTER_1 = 0x42; static const uint8_t START_CHARACTER_2 = 0x4D; -static const uint16_t PMS_STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms +static const uint16_t STABILISING_MS = 30000; // time taken for the sensor to become stable after power on in ms -static const uint16_t PMS_CMD_MEASUREMENT_MODE_PASSIVE = - 0x0000; // use `PMS_CMD_MANUAL_MEASUREMENT` to trigger a measurement -static const uint16_t PMS_CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements -static const uint16_t PMS_CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode -static const uint16_t PMS_CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode +static const uint16_t CMD_MEASUREMENT_MODE_PASSIVE = + 0x0000; // use `Command::MANUAL_MEASUREMENT` to trigger a measurement +static const uint16_t CMD_MEASUREMENT_MODE_ACTIVE = 0x0001; // automatically perform measurements +static const uint16_t CMD_SLEEP_MODE_SLEEP = 0x0000; // go to sleep mode +static const uint16_t CMD_SLEEP_MODE_WAKEUP = 0x0001; // wake up from sleep mode void PMSX003Component::setup() {} @@ -42,7 +41,7 @@ void PMSX003Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); - if (this->update_interval_ <= PMS_STABILISING_MS) { + if (this->update_interval_ <= STABILISING_MS) { ESP_LOGCONFIG(TAG, " Mode: active continuous (sensor default)"); } else { ESP_LOGCONFIG(TAG, " Mode: passive with sleep/wake cycles"); @@ -55,44 +54,44 @@ void PMSX003Component::loop() { const uint32_t now = App.get_loop_component_start_time(); // Initialize sensor mode on first loop - if (this->initialised_ == 0) { - if (this->update_interval_ > PMS_STABILISING_MS) { + if (!this->initialised_) { + if (this->update_interval_ > STABILISING_MS) { // Long update interval: use passive mode with sleep/wake cycles - this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_PASSIVE); - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); + this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_PASSIVE); + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP); } else { // Short/zero update interval: use active continuous mode - this->send_command_(PMS_CMD_MEASUREMENT_MODE, PMS_CMD_MEASUREMENT_MODE_ACTIVE); + this->send_command_(Command::MEASUREMENT_MODE, CMD_MEASUREMENT_MODE_ACTIVE); } - this->initialised_ = 1; + this->initialised_ = true; } // If we update less often than it takes the device to stabilise, spin the fan down // rather than running it constantly. It does take some time to stabilise, so we // need to keep track of what state we're in. - if (this->update_interval_ > PMS_STABILISING_MS) { + if (this->update_interval_ > STABILISING_MS) { switch (this->state_) { - case PMSX003_STATE_IDLE: + case State::IDLE: // Power on the sensor now so it'll be ready when we hit the update time - if (now - this->last_update_ < (this->update_interval_ - PMS_STABILISING_MS)) + if (now - this->last_update_ < (this->update_interval_ - STABILISING_MS)) return; - this->state_ = PMSX003_STATE_STABILISING; - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_WAKEUP); + this->state_ = State::STABILISING; + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_WAKEUP); this->fan_on_time_ = now; return; - case PMSX003_STATE_STABILISING: + case State::STABILISING: // wait for the sensor to be stable - if (now - this->fan_on_time_ < PMS_STABILISING_MS) + if (now - this->fan_on_time_ < STABILISING_MS) return; // consume any command responses that are in the serial buffer while (this->available()) this->read_byte(&this->data_[0]); // Trigger a new read - this->send_command_(PMS_CMD_MANUAL_MEASUREMENT, 0); - this->state_ = PMSX003_STATE_WAITING; + this->send_command_(Command::MANUAL_MEASUREMENT, 0); + this->state_ = State::WAITING; break; - case PMSX003_STATE_WAITING: + case State::WAITING: // Just go ahead and read stuff break; } @@ -180,27 +179,31 @@ optional PMSX003Component::check_byte_() { } bool PMSX003Component::check_payload_length_(uint16_t payload_length) { + // https://avaldebe.github.io/PyPMS/sensors/Plantower/ switch (this->type_) { - case PMSX003_TYPE_X003: - // The expected payload length is typically 28 bytes. - // However, a 20-byte payload check was already present in the code. - // No official documentation was found confirming this. - // Retaining this check to avoid breaking existing behavior. + case Type::PMS1003: + return payload_length == 28; // 2*13+2 + case Type::PMS3003: // Data 7/8/9 not set/reserved + return payload_length == 20; // 2*9+2 + case Type::PMSX003: // Data 13 not set/reserved + // Deprecated: Length 20 is for PMS3003 backwards compatibility return payload_length == 28 || payload_length == 20; // 2*13+2 - case PMSX003_TYPE_5003T: - case PMSX003_TYPE_5003S: - return payload_length == 28; // 2*13+2 (Data 13 not set/reserved) - case PMSX003_TYPE_5003ST: - return payload_length == 36; // 2*17+2 (Data 16 not set/reserved) + case Type::PMS5003S: + case Type::PMS5003T: // Data 13 not set/reserved + return payload_length == 28; // 2*13+2 + case Type::PMS5003ST: // Data 16 not set/reserved + return payload_length == 36; // 2*17+2 + case Type::PMS9003M: + return payload_length == 28; // 2*13+2 } return false; } -void PMSX003Component::send_command_(PMSX0003Command cmd, uint16_t data) { +void PMSX003Component::send_command_(Command cmd, uint16_t data) { uint8_t send_data[7] = { START_CHARACTER_1, // Start Byte 1 START_CHARACTER_2, // Start Byte 2 - cmd, // Command + static_cast(cmd), // Command uint8_t((data >> 8) & 0xFF), // Data 1 uint8_t((data >> 0) & 0xFF), // Data 2 0, // Verify Byte 1 @@ -265,7 +268,7 @@ void PMSX003Component::parse_data_() { if (this->pm_particles_25um_sensor_ != nullptr) this->pm_particles_25um_sensor_->publish_state(pm_particles_25um); - if (this->type_ == PMSX003_TYPE_5003T) { + if (this->type_ == Type::PMS5003T) { ESP_LOGD(TAG, "Got PM0.3 Particles: %u Count/0.1L, PM0.5 Particles: %u Count/0.1L, PM1.0 Particles: %u Count/0.1L, " "PM2.5 Particles %u Count/0.1L", @@ -289,7 +292,7 @@ void PMSX003Component::parse_data_() { } // Formaldehyde - if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003S) { + if (this->type_ == Type::PMS5003S || this->type_ == Type::PMS5003ST) { const uint16_t formaldehyde = this->get_16_bit_uint_(28); ESP_LOGD(TAG, "Got Formaldehyde: %u µg/m^3", formaldehyde); @@ -299,8 +302,8 @@ void PMSX003Component::parse_data_() { } // Temperature and Humidity - if (this->type_ == PMSX003_TYPE_5003ST || this->type_ == PMSX003_TYPE_5003T) { - const uint8_t temperature_offset = (this->type_ == PMSX003_TYPE_5003T) ? 24 : 30; + if (this->type_ == Type::PMS5003T || this->type_ == Type::PMS5003ST) { + const uint8_t temperature_offset = (this->type_ == Type::PMS5003T) ? 24 : 30; const float temperature = static_cast(this->get_16_bit_uint_(temperature_offset)) / 10.0f; const float humidity = this->get_16_bit_uint_(temperature_offset + 2) / 10.0f; @@ -314,22 +317,22 @@ void PMSX003Component::parse_data_() { } // Firmware Version and Error Code - if (this->type_ == PMSX003_TYPE_5003ST) { - const uint8_t firmware_version = this->data_[36]; - const uint8_t error_code = this->data_[37]; + if (this->type_ == Type::PMS1003 || this->type_ == Type::PMS5003ST || this->type_ == Type::PMS9003M) { + const uint8_t firmware_error_code_offset = (this->type_ == Type::PMS5003ST) ? 36 : 28; + const uint8_t firmware_version = this->data_[firmware_error_code_offset]; + const uint8_t error_code = this->data_[firmware_error_code_offset + 1]; ESP_LOGD(TAG, "Got Firmware Version: 0x%02X, Error Code: 0x%02X", firmware_version, error_code); } // Spin down the sensor again if we aren't going to need it until more time has // passed than it takes to stabilise - if (this->update_interval_ > PMS_STABILISING_MS) { - this->send_command_(PMS_CMD_SLEEP_MODE, PMS_CMD_SLEEP_MODE_SLEEP); - this->state_ = PMSX003_STATE_IDLE; + if (this->update_interval_ > STABILISING_MS) { + this->send_command_(Command::SLEEP_MODE, CMD_SLEEP_MODE_SLEEP); + this->state_ = State::IDLE; } this->status_clear_warning(); } -} // namespace pmsx003 -} // namespace esphome +} // namespace esphome::pmsx003 diff --git a/esphome/components/pmsx003/pmsx003.h b/esphome/components/pmsx003/pmsx003.h index f48121800e..d559f2dec0 100644 --- a/esphome/components/pmsx003/pmsx003.h +++ b/esphome/components/pmsx003/pmsx003.h @@ -5,27 +5,28 @@ #include "esphome/components/sensor/sensor.h" #include "esphome/components/uart/uart.h" -namespace esphome { -namespace pmsx003 { +namespace esphome::pmsx003 { -enum PMSX0003Command : uint8_t { - PMS_CMD_MEASUREMENT_MODE = - 0xE1, // Data Options: `PMS_CMD_MEASUREMENT_MODE_PASSIVE`, `PMS_CMD_MEASUREMENT_MODE_ACTIVE` - PMS_CMD_MANUAL_MEASUREMENT = 0xE2, - PMS_CMD_SLEEP_MODE = 0xE4, // Data Options: `PMS_CMD_SLEEP_MODE_SLEEP`, `PMS_CMD_SLEEP_MODE_WAKEUP` +enum class Type : uint8_t { + PMS1003 = 0, + PMS3003, + PMSX003, // PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component) + PMS5003S, + PMS5003T, + PMS5003ST, + PMS9003M, }; -enum PMSX003Type { - PMSX003_TYPE_X003 = 0, - PMSX003_TYPE_5003T, - PMSX003_TYPE_5003ST, - PMSX003_TYPE_5003S, +enum class Command : uint8_t { + MEASUREMENT_MODE = 0xE1, // Data Options: `CMD_MEASUREMENT_MODE_PASSIVE`, `CMD_MEASUREMENT_MODE_ACTIVE` + MANUAL_MEASUREMENT = 0xE2, + SLEEP_MODE = 0xE4, // Data Options: `CMD_SLEEP_MODE_SLEEP`, `CMD_SLEEP_MODE_WAKEUP` }; -enum PMSX003State { - PMSX003_STATE_IDLE = 0, - PMSX003_STATE_STABILISING, - PMSX003_STATE_WAITING, +enum class State : uint8_t { + IDLE = 0, + STABILISING, + WAITING, }; class PMSX003Component : public uart::UARTDevice, public Component { @@ -37,7 +38,7 @@ class PMSX003Component : public uart::UARTDevice, public Component { void set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; } - void set_type(PMSX003Type type) { this->type_ = type; } + void set_type(Type type) { this->type_ = type; } void set_pm_1_0_std_sensor(sensor::Sensor *pm_1_0_std_sensor) { this->pm_1_0_std_sensor_ = pm_1_0_std_sensor; } void set_pm_2_5_std_sensor(sensor::Sensor *pm_2_5_std_sensor) { this->pm_2_5_std_sensor_ = pm_2_5_std_sensor; } @@ -77,20 +78,20 @@ class PMSX003Component : public uart::UARTDevice, public Component { optional check_byte_(); void parse_data_(); bool check_payload_length_(uint16_t payload_length); - void send_command_(PMSX0003Command cmd, uint16_t data); + void send_command_(Command cmd, uint16_t data); uint16_t get_16_bit_uint_(uint8_t start_index) const { return encode_uint16(this->data_[start_index], this->data_[start_index + 1]); } + Type type_; + State state_{State::IDLE}; + bool initialised_{false}; uint8_t data_[64]; uint8_t data_index_{0}; - uint8_t initialised_{0}; uint32_t fan_on_time_{0}; uint32_t last_update_{0}; uint32_t last_transmission_{0}; uint32_t update_interval_{0}; - PMSX003State state_{PMSX003_STATE_IDLE}; - PMSX003Type type_; // "Standard Particle" sensor::Sensor *pm_1_0_std_sensor_{nullptr}; @@ -118,5 +119,4 @@ class PMSX003Component : public uart::UARTDevice, public Component { sensor::Sensor *humidity_sensor_{nullptr}; }; -} // namespace pmsx003 -} // namespace esphome +} // namespace esphome::pmsx003 diff --git a/esphome/components/pmsx003/sensor.py b/esphome/components/pmsx003/sensor.py index bebd3a01ee..cdcedc85ac 100644 --- a/esphome/components/pmsx003/sensor.py +++ b/esphome/components/pmsx003/sensor.py @@ -40,34 +40,128 @@ pmsx003_ns = cg.esphome_ns.namespace("pmsx003") PMSX003Component = pmsx003_ns.class_("PMSX003Component", uart.UARTDevice, cg.Component) PMSX003Sensor = pmsx003_ns.class_("PMSX003Sensor", sensor.Sensor) -TYPE_PMSX003 = "PMSX003" +TYPE_PMS1003 = "PMS1003" +TYPE_PMS3003 = "PMS3003" +TYPE_PMSX003 = "PMSX003" # PMS5003, PMS6003, PMS7003, PMSA003 (NOT PMSA003I - see `pmsa003i` component) +TYPE_PMS5003S = "PMS5003S" TYPE_PMS5003T = "PMS5003T" TYPE_PMS5003ST = "PMS5003ST" -TYPE_PMS5003S = "PMS5003S" +TYPE_PMS9003M = "PMS9003M" -PMSX003Type = pmsx003_ns.enum("PMSX003Type") +Type = pmsx003_ns.enum("Type", is_class=True) PMSX003_TYPES = { - TYPE_PMSX003: PMSX003Type.PMSX003_TYPE_X003, - TYPE_PMS5003T: PMSX003Type.PMSX003_TYPE_5003T, - TYPE_PMS5003ST: PMSX003Type.PMSX003_TYPE_5003ST, - TYPE_PMS5003S: PMSX003Type.PMSX003_TYPE_5003S, + TYPE_PMS1003: Type.PMS1003, + TYPE_PMS3003: Type.PMS3003, + TYPE_PMSX003: Type.PMSX003, + TYPE_PMS5003S: Type.PMS5003S, + TYPE_PMS5003T: Type.PMS5003T, + TYPE_PMS5003ST: Type.PMS5003ST, + TYPE_PMS9003M: Type.PMS9003M, } SENSORS_TO_TYPE = { - CONF_PM_1_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_1_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0_STD: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_0_3UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_0_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_1_0UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_2_5UM: [TYPE_PMSX003, TYPE_PMS5003T, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_5_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_PM_10_0UM: [TYPE_PMSX003, TYPE_PMS5003ST, TYPE_PMS5003S], - CONF_FORMALDEHYDE: [TYPE_PMS5003ST, TYPE_PMS5003S], + CONF_PM_1_0_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0_STD: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_1_0: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0: [ + TYPE_PMS1003, + TYPE_PMS3003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_0_3UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_0_5UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_1_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_2_5UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003T, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_5_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_PM_10_0UM: [ + TYPE_PMS1003, + TYPE_PMSX003, + TYPE_PMS5003S, + TYPE_PMS5003ST, + TYPE_PMS9003M, + ], + CONF_FORMALDEHYDE: [TYPE_PMS5003S, TYPE_PMS5003ST], CONF_TEMPERATURE: [TYPE_PMS5003T, TYPE_PMS5003ST], CONF_HUMIDITY: [TYPE_PMS5003T, TYPE_PMS5003ST], } diff --git a/esphome/components/pmwcs3/pmwcs3.cpp b/esphome/components/pmwcs3/pmwcs3.cpp index 95638851b5..2ed7789c53 100644 --- a/esphome/components/pmwcs3/pmwcs3.cpp +++ b/esphome/components/pmwcs3/pmwcs3.cpp @@ -53,8 +53,6 @@ void PMWCS3Component::water_calibration() { void PMWCS3Component::update() { this->read_data_(); } -float PMWCS3Component::get_setup_priority() const { return setup_priority::DATA; } - void PMWCS3Component::dump_config() { ESP_LOGCONFIG(TAG, "PMWCS3"); LOG_I2C_DEVICE(this); diff --git a/esphome/components/pmwcs3/pmwcs3.h b/esphome/components/pmwcs3/pmwcs3.h index d63c516586..b1e26eec4f 100644 --- a/esphome/components/pmwcs3/pmwcs3.h +++ b/esphome/components/pmwcs3/pmwcs3.h @@ -14,7 +14,6 @@ class PMWCS3Component : public PollingComponent, public i2c::I2CDevice { public: void update() override; void dump_config() override; - float get_setup_priority() const override; void set_e25_sensor(sensor::Sensor *e25_sensor) { e25_sensor_ = e25_sensor; } void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; } diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index 733810c242..5366aab54e 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -426,8 +426,6 @@ bool PN532::write_tag_(nfc::NfcTagUid &uid, nfc::NdefMessage *message) { return false; } -float PN532::get_setup_priority() const { return setup_priority::DATA; } - void PN532::dump_config() { ESP_LOGCONFIG(TAG, "PN532:"); switch (this->error_code_) { diff --git a/esphome/components/pn532/pn532.h b/esphome/components/pn532/pn532.h index 488ec4af3b..73a6c15164 100644 --- a/esphome/components/pn532/pn532.h +++ b/esphome/components/pn532/pn532.h @@ -35,7 +35,6 @@ class PN532 : public PollingComponent { void dump_config() override; void update() override; - float get_setup_priority() const override; void loop() override; void on_powerdown() override { powerdown(); } diff --git a/esphome/components/pulse_counter/pulse_counter_sensor.h b/esphome/components/pulse_counter/pulse_counter_sensor.h index 5ba59cca2a..f906e9e5cb 100644 --- a/esphome/components/pulse_counter/pulse_counter_sensor.h +++ b/esphome/components/pulse_counter/pulse_counter_sensor.h @@ -6,6 +6,10 @@ #include +// TODO: Migrate from legacy PCNT API (driver/pcnt.h) to new PCNT API (driver/pulse_cnt.h) +// The legacy PCNT API is deprecated in ESP-IDF 5.x. Migration would allow removing the +// "driver" IDF component dependency. See: +// https://docs.espressif.com/projects/esp-idf/en/latest/esp32/migration-guides/release-5.x/5.0/peripherals.html#id6 #if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C3) #include #define HAS_PCNT diff --git a/esphome/components/pulse_counter/sensor.py b/esphome/components/pulse_counter/sensor.py index dbf67fd2ad..65be5ee793 100644 --- a/esphome/components/pulse_counter/sensor.py +++ b/esphome/components/pulse_counter/sensor.py @@ -1,6 +1,7 @@ from esphome import automation, pins import esphome.codegen as cg from esphome.components import sensor +from esphome.components.esp32 import include_builtin_idf_component import esphome.config_validation as cv from esphome.const import ( CONF_COUNT_MODE, @@ -126,7 +127,14 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = await sensor.new_sensor(config, config.get(CONF_USE_PCNT)) + use_pcnt = config.get(CONF_USE_PCNT) + if CORE.is_esp32 and use_pcnt: + # Re-enable ESP-IDF's legacy driver component (excluded by default to save compile time) + # Provides driver/pcnt.h header for hardware pulse counter API + # TODO: Remove this once pulse_counter migrates to new PCNT API (driver/pulse_cnt.h) + include_builtin_idf_component("driver") + + var = await sensor.new_sensor(config, use_pcnt) await cg.register_component(var, config) pin = await cg.gpio_pin_expression(config[CONF_PIN]) diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.cpp b/esphome/components/pulse_meter/pulse_meter_sensor.cpp index 9a7630a7be..007deb66e5 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.cpp +++ b/esphome/components/pulse_meter/pulse_meter_sensor.cpp @@ -125,8 +125,6 @@ void PulseMeterSensor::loop() { } } -float PulseMeterSensor::get_setup_priority() const { return setup_priority::DATA; } - void PulseMeterSensor::dump_config() { LOG_SENSOR("", "Pulse Meter", this); LOG_PIN(" Pin: ", this->pin_); diff --git a/esphome/components/pulse_meter/pulse_meter_sensor.h b/esphome/components/pulse_meter/pulse_meter_sensor.h index 748bab29ac..5800c4ec42 100644 --- a/esphome/components/pulse_meter/pulse_meter_sensor.h +++ b/esphome/components/pulse_meter/pulse_meter_sensor.h @@ -27,7 +27,6 @@ class PulseMeterSensor : public sensor::Sensor, public Component { void setup() override; void loop() override; - float get_setup_priority() const override; void dump_config() override; protected: diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 74b7caefb2..1dc7caaf16 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -2,6 +2,28 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +// Helper macros +#define PARSE_INT(field, field_name) \ + { \ + get_token(token_buf); \ + auto val = parse_number(token_buf); \ + if (val.has_value()) { \ + (field) = val.value(); \ + } else { \ + ESP_LOGD(TAG, "invalid " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + +#define PARSE_STR(field, field_name) \ + { \ + get_token(field); \ + if (strlen(field) < 2) { \ + ESP_LOGD(TAG, "too short " field_name " in line %s", buffer.substr(0, buffer.size() - 2).c_str()); \ + return; \ + } \ + } + namespace esphome { namespace pylontech { @@ -64,40 +86,114 @@ void PylontechComponent::loop() { void PylontechComponent::process_line_(std::string &buffer) { ESP_LOGV(TAG, "Read from serial: %s", buffer.substr(0, buffer.size() - 2).c_str()); // clang-format off - // example line to parse: - // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St - // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // example lines to parse: + // Power Volt Curr Tempr Tlow Thigh Vlow Vhigh Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St + // 1 50548 8910 25000 24200 25000 3368 3371 Charge Normal Normal Normal 97% 2021-06-30 20:49:45 Normal Normal 22700 Normal + // 1 46012 1255 9100 5300 5500 3047 3091 SysError Low Normal Normal 4% 2025-11-28 17:56:33 Low Normal 7800 Normal + // newer firmware example: + // Power Volt Curr Tempr Tlow Tlow.Id Thigh Thigh.Id Vlow Vlow.Id Vhigh Vhigh.Id Base.St Volt.St Curr.St Temp.St Coulomb Time B.V.St B.T.St MosTempr M.T.St SysAlarm.St + // 1 49405 0 17600 13700 8 14500 0 3293 2 3294 0 Idle Normal Normal Normal 60% 2025-12-05 00:53:41 Normal Normal 16600 Normal Normal // clang-format on PylontechListener::LineContents l{}; - char mostempr_s[6]; - const int parsed = sscanf( // NOLINT - buffer.c_str(), "%d %d %d %d %d %d %d %d %7s %7s %7s %7s %d%% %*d-%*d-%*d %*d:%*d:%*d %*s %*s %5s %*s", // NOLINT - &l.bat_num, &l.volt, &l.curr, &l.tempr, &l.tlow, &l.thigh, &l.vlow, &l.vhigh, l.base_st, l.volt_st, // NOLINT - l.curr_st, l.temp_st, &l.coulomb, mostempr_s); // NOLINT - if (l.bat_num <= 0) { - ESP_LOGD(TAG, "invalid bat_num in line %s", buffer.substr(0, buffer.size() - 2).c_str()); - return; + const char *cursor = buffer.c_str(); + char token_buf[TEXT_SENSOR_MAX_LEN] = {0}; + + // Helper Lambda to extract tokens + auto get_token = [&](char *token_buf) -> void { + // Skip leading whitespace + while (*cursor == ' ' || *cursor == '\t') { + cursor++; + } + + if (*cursor == '\0') { + token_buf[0] = 0; + return; + } + + const char *start = cursor; + + // Find end of field + while (*cursor != '\0' && *cursor != ' ' && *cursor != '\t' && *cursor != '\r') { + cursor++; + } + + size_t token_len = std::min(static_cast(cursor - start), static_cast(TEXT_SENSOR_MAX_LEN - 1)); + memcpy(token_buf, start, token_len); + token_buf[token_len] = 0; + }; + + { + get_token(token_buf); + auto val = parse_number(token_buf); + if (val.has_value() && val.value() > 0) { + l.bat_num = val.value(); + } else if (strcmp(token_buf, "Power") == 0) { + // header line i.e. "Power Volt Curr" and so on + this->has_tlow_id_ = buffer.find("Tlow.Id") != std::string::npos; + ESP_LOGD(TAG, "header line %s Tlow.Id: %s", this->has_tlow_id_ ? "with" : "without", + buffer.substr(0, buffer.size() - 2).c_str()); + return; + } else { + ESP_LOGD(TAG, "unknown line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } } - if (parsed != 14) { - ESP_LOGW(TAG, "invalid line: found only %d items in %s", parsed, buffer.substr(0, buffer.size() - 2).c_str()); - return; + PARSE_INT(l.volt, "Volt"); + PARSE_INT(l.curr, "Curr"); + PARSE_INT(l.tempr, "Tempr"); + PARSE_INT(l.tlow, "Tlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Tlow.Id } - auto mostempr_parsed = parse_number(mostempr_s); - if (mostempr_parsed.has_value()) { - l.mostempr = mostempr_parsed.value(); - } else { - l.mostempr = -300; - ESP_LOGW(TAG, "bat_num %d: received no mostempr", l.bat_num); + PARSE_INT(l.thigh, "Thigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Thigh.Id } + PARSE_INT(l.vlow, "Vlow"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vlow.Id + } + PARSE_INT(l.vhigh, "Vhigh"); + if (this->has_tlow_id_) { + get_token(token_buf); // Skip Vhigh.Id + } + PARSE_STR(l.base_st, "Base.St"); + PARSE_STR(l.volt_st, "Volt.St"); + PARSE_STR(l.curr_st, "Curr.St"); + PARSE_STR(l.temp_st, "Temp.St"); + { + get_token(token_buf); + for (char &i : token_buf) { + if (i == '%') { + i = 0; + break; + } + } + auto coul_val = parse_number(token_buf); + if (coul_val.has_value()) { + l.coulomb = coul_val.value(); + } else { + ESP_LOGD(TAG, "invalid Coulomb in line %s", buffer.substr(0, buffer.size() - 2).c_str()); + return; + } + } + get_token(token_buf); // Skip Date + get_token(token_buf); // Skip Time + get_token(token_buf); // Skip B.V.St + get_token(token_buf); // Skip B.T.St + PARSE_INT(l.mostempr, "Mostempr"); + + ESP_LOGD(TAG, "successful line %s", buffer.substr(0, buffer.size() - 2).c_str()); for (PylontechListener *listener : this->listeners_) { listener->on_line_read(&l); } } -float PylontechComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace pylontech } // namespace esphome + +#undef PARSE_INT +#undef PARSE_STR diff --git a/esphome/components/pylontech/pylontech.h b/esphome/components/pylontech/pylontech.h index 3282cb4d9f..5727928a60 100644 --- a/esphome/components/pylontech/pylontech.h +++ b/esphome/components/pylontech/pylontech.h @@ -8,14 +8,14 @@ namespace esphome { namespace pylontech { static const uint8_t NUM_BUFFERS = 20; -static const uint8_t TEXT_SENSOR_MAX_LEN = 8; +static const uint8_t TEXT_SENSOR_MAX_LEN = 14; class PylontechListener { public: struct LineContents { int bat_num = 0, volt, curr, tempr, tlow, thigh, vlow, vhigh, coulomb, mostempr; - char base_st[TEXT_SENSOR_MAX_LEN], volt_st[TEXT_SENSOR_MAX_LEN], curr_st[TEXT_SENSOR_MAX_LEN], - temp_st[TEXT_SENSOR_MAX_LEN]; + char base_st[TEXT_SENSOR_MAX_LEN] = {0}, volt_st[TEXT_SENSOR_MAX_LEN] = {0}, curr_st[TEXT_SENSOR_MAX_LEN] = {0}, + temp_st[TEXT_SENSOR_MAX_LEN] = {0}; }; virtual void on_line_read(LineContents *line); @@ -34,8 +34,6 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { void setup() override; void dump_config() override; - float get_setup_priority() const override; - void register_listener(PylontechListener *listener) { this->listeners_.push_back(listener); } protected: @@ -45,6 +43,7 @@ class PylontechComponent : public PollingComponent, public uart::UARTDevice { std::string buffer_[NUM_BUFFERS]; int buffer_index_write_ = 0; int buffer_index_read_ = 0; + bool has_tlow_id_ = false; std::vector listeners_{}; }; diff --git a/esphome/components/qmc5883l/qmc5883l.cpp b/esphome/components/qmc5883l/qmc5883l.cpp index 693614581c..bc2adb5cfe 100644 --- a/esphome/components/qmc5883l/qmc5883l.cpp +++ b/esphome/components/qmc5883l/qmc5883l.cpp @@ -86,8 +86,6 @@ void QMC5883LComponent::dump_config() { LOG_PIN(" DRDY Pin: ", this->drdy_pin_); } -float QMC5883LComponent::get_setup_priority() const { return setup_priority::DATA; } - void QMC5883LComponent::update() { i2c::ErrorCode err; uint8_t status = false; diff --git a/esphome/components/qmc5883l/qmc5883l.h b/esphome/components/qmc5883l/qmc5883l.h index 5ba7180e23..21ef9c2a17 100644 --- a/esphome/components/qmc5883l/qmc5883l.h +++ b/esphome/components/qmc5883l/qmc5883l.h @@ -31,7 +31,6 @@ class QMC5883LComponent : public PollingComponent, public i2c::I2CDevice { public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_drdy_pin(GPIOPin *pin) { drdy_pin_ = pin; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index ba05abe8e0..090e4dcf32 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -132,18 +132,15 @@ void RD03DComponent::process_frame_() { // Header is 4 bytes, each target is 8 bytes uint8_t offset = FRAME_HEADER_SIZE + (i * TARGET_DATA_SIZE); - // Extract raw bytes for this target - // Note: Despite datasheet Table 5-2 showing order as X, Y, Speed, Resolution, - // actual radar output has Resolution before Speed (verified empirically - - // stationary targets were showing non-zero speed with original field order) + // Extract raw bytes for this target (per datasheet Table 5-2: X, Y, Speed, Resolution) uint8_t x_low = this->buffer_[offset + 0]; uint8_t x_high = this->buffer_[offset + 1]; uint8_t y_low = this->buffer_[offset + 2]; uint8_t y_high = this->buffer_[offset + 3]; - uint8_t res_low = this->buffer_[offset + 4]; - uint8_t res_high = this->buffer_[offset + 5]; - uint8_t speed_low = this->buffer_[offset + 6]; - uint8_t speed_high = this->buffer_[offset + 7]; + uint8_t speed_low = this->buffer_[offset + 4]; + uint8_t speed_high = this->buffer_[offset + 5]; + uint8_t res_low = this->buffer_[offset + 6]; + uint8_t res_high = this->buffer_[offset + 7]; // Decode values per RD-03D format int16_t x = decode_value(x_low, x_high); diff --git a/esphome/components/rd03d/rd03d.h b/esphome/components/rd03d/rd03d.h index 7413fe38f2..8bf7b423be 100644 --- a/esphome/components/rd03d/rd03d.h +++ b/esphome/components/rd03d/rd03d.h @@ -42,7 +42,6 @@ class RD03DComponent : public Component, public uart::UARTDevice { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } #ifdef USE_SENSOR void set_target_count_sensor(sensor::Sensor *sensor) { this->target_count_sensor_ = sensor; } diff --git a/esphome/components/rdm6300/rdm6300.cpp b/esphome/components/rdm6300/rdm6300.cpp index f9b6126c4b..04065982f6 100644 --- a/esphome/components/rdm6300/rdm6300.cpp +++ b/esphome/components/rdm6300/rdm6300.cpp @@ -34,6 +34,8 @@ void rdm6300::RDM6300Component::loop() { this->buffer_[this->read_state_ / 2] += value; } this->read_state_++; + } else if (data == 0x0D || data == 0x0A) { + // Skip CR/LF bytes (ID-20LA compatibility) } else if (data != RDM6300_END_BYTE) { ESP_LOGW(TAG, "Invalid end byte from RDM6300!"); this->read_state_ = RDM6300_STATE_WAITING_FOR_START; diff --git a/esphome/components/remote_receiver/__init__.py b/esphome/components/remote_receiver/__init__.py index f5d89f2f0f..b3dc213c5f 100644 --- a/esphome/components/remote_receiver/__init__.py +++ b/esphome/components/remote_receiver/__init__.py @@ -170,6 +170,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers( async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_driver_rmt") + var = cg.new_Pvariable(config[CONF_ID], pin) cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) cg.add(var.set_receive_symbols(config[CONF_RECEIVE_SYMBOLS])) diff --git a/esphome/components/remote_transmitter/__init__.py b/esphome/components/remote_transmitter/__init__.py index f182a1ec0d..8383b9dd75 100644 --- a/esphome/components/remote_transmitter/__init__.py +++ b/esphome/components/remote_transmitter/__init__.py @@ -112,6 +112,9 @@ async def digital_write_action_to_code(config, action_id, template_arg, args): async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) if CORE.is_esp32: + # Re-enable ESP-IDF's RMT driver (excluded by default to save compile time) + esp32.include_builtin_idf_component("esp_driver_rmt") + 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])) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index f20789fb9f..d35541e2e1 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -83,7 +83,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen uint32_t on_time, off_time; this->calculate_on_off_time_(this->temp_.get_carrier_frequency(), &on_time, &off_time); this->target_time_ = 0; - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); for (uint32_t i = 0; i < send_times; i++) { InterruptLock lock; for (int32_t item : this->temp_.get_data()) { @@ -102,7 +102,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) this->target_time_ += send_wait; } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter.h b/esphome/components/remote_transmitter/remote_transmitter.h index dd6a849e4c..65bd2ac8b2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.h +++ b/esphome/components/remote_transmitter/remote_transmitter.h @@ -57,8 +57,8 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } #endif - Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; - Trigger<> *get_complete_trigger() const { return this->complete_trigger_; }; + Trigger<> *get_transmit_trigger() { return &this->transmit_trigger_; } + Trigger<> *get_complete_trigger() { return &this->complete_trigger_; } protected: void send_internal(uint32_t send_times, uint32_t send_wait) override; @@ -96,8 +96,8 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, #endif uint8_t carrier_duty_percent_; - Trigger<> *transmit_trigger_{new Trigger<>()}; - Trigger<> *complete_trigger_{new Trigger<>()}; + Trigger<> transmit_trigger_; + Trigger<> complete_trigger_; }; } // namespace remote_transmitter diff --git a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp index 59c85c99a8..89d97895b2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter_esp32.cpp @@ -203,7 +203,7 @@ void RemoteTransmitterComponent::wait_for_rmt_() { this->status_set_warning(); } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) @@ -264,7 +264,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen return; } - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -333,7 +333,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen ESP_LOGE(TAG, "Empty data"); return; } - this->transmit_trigger_->trigger(); + this->transmit_trigger_.trigger(); for (uint32_t i = 0; i < send_times; i++) { rmt_transmit_config_t config; memset(&config, 0, sizeof(config)); @@ -354,7 +354,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen if (i + 1 < send_times) delayMicroseconds(send_wait); } - this->complete_trigger_->trigger(); + this->complete_trigger_.trigger(); } #endif diff --git a/esphome/components/resampler/speaker/resampler_speaker.h b/esphome/components/resampler/speaker/resampler_speaker.h index 51790069d2..810087ab7f 100644 --- a/esphome/components/resampler/speaker/resampler_speaker.h +++ b/esphome/components/resampler/speaker/resampler_speaker.h @@ -16,7 +16,6 @@ namespace resampler { class ResamplerSpeaker : public Component, public speaker::Speaker { public: - float get_setup_priority() const override { return esphome::setup_priority::DATA; } void setup() override; void loop() override; diff --git a/esphome/components/rotary_encoder/rotary_encoder.cpp b/esphome/components/rotary_encoder/rotary_encoder.cpp index c652944120..38fd14375d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.cpp +++ b/esphome/components/rotary_encoder/rotary_encoder.cpp @@ -235,7 +235,6 @@ void RotaryEncoderSensor::loop() { } } -float RotaryEncoderSensor::get_setup_priority() const { return setup_priority::DATA; } void RotaryEncoderSensor::set_restore_mode(RotaryEncoderRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } diff --git a/esphome/components/rotary_encoder/rotary_encoder.h b/esphome/components/rotary_encoder/rotary_encoder.h index 14442f0565..865554cd4d 100644 --- a/esphome/components/rotary_encoder/rotary_encoder.h +++ b/esphome/components/rotary_encoder/rotary_encoder.h @@ -82,8 +82,6 @@ class RotaryEncoderSensor : public sensor::Sensor, public Component { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void add_on_clockwise_callback(std::function callback) { this->on_clockwise_callback_.add(std::move(callback)); } diff --git a/esphome/components/rp2040/preferences.cpp b/esphome/components/rp2040/preferences.cpp index e84033bc52..172da32adc 100644 --- a/esphome/components/rp2040/preferences.cpp +++ b/esphome/components/rp2040/preferences.cpp @@ -18,11 +18,12 @@ namespace rp2040 { static const char *const TAG = "rp2040.preferences"; -static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static uint8_t *s_flash_storage = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) -static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static constexpr uint32_t RP2040_FLASH_STORAGE_SIZE = 512; -static const uint32_t RP2040_FLASH_STORAGE_SIZE = 512; +static bool s_prevent_write = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static uint8_t + s_flash_storage[RP2040_FLASH_STORAGE_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) +static bool s_flash_dirty = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) // Stack buffer size for preferences - covers virtually all real-world preferences without heap allocation static constexpr size_t PREF_BUFFER_SIZE = 64; @@ -91,7 +92,6 @@ class RP2040Preferences : public ESPPreferences { RP2040Preferences() : eeprom_sector_(&_EEPROM_start) {} void setup() { - s_flash_storage = new uint8_t[RP2040_FLASH_STORAGE_SIZE]; // NOLINT ESP_LOGVV(TAG, "Loading preferences from flash"); memcpy(s_flash_storage, this->eeprom_sector_, RP2040_FLASH_STORAGE_SIZE); } @@ -149,10 +149,11 @@ class RP2040Preferences : public ESPPreferences { uint8_t *eeprom_sector_; }; +static RP2040Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new RP2040Preferences(); // NOLINT(cppcoreguidelines-owning-memory) - prefs->setup(); - global_preferences = prefs; + s_preferences.setup(); + global_preferences = &s_preferences; } void preferences_prevent_write(bool prevent) { s_prevent_write = prevent; } diff --git a/esphome/components/rtttl/rtttl.cpp b/esphome/components/rtttl/rtttl.cpp index 65fcc207d4..c179282c50 100644 --- a/esphome/components/rtttl/rtttl.cpp +++ b/esphome/components/rtttl/rtttl.cpp @@ -290,25 +290,26 @@ void Rtttl::loop() { this->position_++; } - // now, get optional '.' dotted note - if (this->rtttl_[this->position_] == '.') { - this->note_duration_ += this->note_duration_ / 2; - this->position_++; - } - // now, get scale uint8_t scale = get_integer_(); - if (scale == 0) + if (scale == 0) { scale = this->default_octave_; + } if (scale < 4 || scale > 7) { ESP_LOGE(TAG, "Octave must be between 4 and 7 (it is %d)", scale); this->finish_(); return; } - bool need_note_gap = false; + + // now, get optional '.' dotted note + if (this->rtttl_[this->position_] == '.') { + this->note_duration_ += this->note_duration_ / 2; + this->position_++; + } // Now play the note + bool need_note_gap = false; if (note) { auto note_index = (scale - 4) * 12 + note; if (note_index < 0 || note_index >= (int) sizeof(NOTES)) { diff --git a/esphome/components/rx8130/rx8130.h b/esphome/components/rx8130/rx8130.h index 6694c763cd..979da3e19c 100644 --- a/esphome/components/rx8130/rx8130.h +++ b/esphome/components/rx8130/rx8130.h @@ -14,8 +14,6 @@ class RX8130Component : public time::RealTimeClock, public i2c::I2CDevice { void dump_config() override; void read_time(); void write_time(); - /// Ensure RTC is initialized at the correct time in the setup sequence - float get_setup_priority() const override { return setup_priority::DATA; } protected: void stop_(bool stop); diff --git a/esphome/components/sdp3x/sdp3x.cpp b/esphome/components/sdp3x/sdp3x.cpp index d4ab04e7cd..6f6cc1ebd8 100644 --- a/esphome/components/sdp3x/sdp3x.cpp +++ b/esphome/components/sdp3x/sdp3x.cpp @@ -114,7 +114,5 @@ void SDP3XComponent::read_pressure_() { this->status_clear_warning(); } -float SDP3XComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace sdp3x } // namespace esphome diff --git a/esphome/components/sdp3x/sdp3x.h b/esphome/components/sdp3x/sdp3x.h index e3d3533c74..afb58d47c8 100644 --- a/esphome/components/sdp3x/sdp3x.h +++ b/esphome/components/sdp3x/sdp3x.h @@ -17,7 +17,6 @@ class SDP3XComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void set_measurement_mode(MeasurementMode mode) { measurement_mode_ = mode; } protected: diff --git a/esphome/components/sds011/sds011.cpp b/esphome/components/sds011/sds011.cpp index 4e12c0e322..cdfd7544ad 100644 --- a/esphome/components/sds011/sds011.cpp +++ b/esphome/components/sds011/sds011.cpp @@ -108,8 +108,6 @@ void SDS011Component::loop() { } } -float SDS011Component::get_setup_priority() const { return setup_priority::DATA; } - void SDS011Component::set_rx_mode_only(bool rx_mode_only) { this->rx_mode_only_ = rx_mode_only; } void SDS011Component::sds011_write_command_(const uint8_t *command_data) { diff --git a/esphome/components/sds011/sds011.h b/esphome/components/sds011/sds011.h index 1f404601b1..3be74e66d1 100644 --- a/esphome/components/sds011/sds011.h +++ b/esphome/components/sds011/sds011.h @@ -21,8 +21,6 @@ class SDS011Component : public Component, public uart::UARTDevice { void dump_config() override; void loop() override; - float get_setup_priority() const override; - void set_update_interval(uint32_t val) { /* ignore */ } void set_update_interval_min(uint8_t update_interval_min); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 3f2be02af2..ae2ee3e3d1 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -2,6 +2,7 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome::sensor { @@ -30,20 +31,13 @@ void log_sensor(const char *tag, const char *prefix, const char *type, Sensor *o } } +// State class strings indexed by StateClass enum (0-4): NONE, MEASUREMENT, TOTAL_INCREASING, TOTAL, MEASUREMENT_ANGLE +PROGMEM_STRING_TABLE(StateClassStrings, "", "measurement", "total_increasing", "total", "measurement_angle"); +static_assert(StateClassStrings::COUNT == STATE_CLASS_LAST + 1, "StateClassStrings must match StateClass enum"); + const LogString *state_class_to_string(StateClass state_class) { - switch (state_class) { - case STATE_CLASS_MEASUREMENT: - return LOG_STR("measurement"); - case STATE_CLASS_TOTAL_INCREASING: - return LOG_STR("total_increasing"); - case STATE_CLASS_TOTAL: - return LOG_STR("total"); - case STATE_CLASS_MEASUREMENT_ANGLE: - return LOG_STR("measurement_angle"); - case STATE_CLASS_NONE: - default: - return LOG_STR(""); - } + // Fallback to index 0 (empty string for STATE_CLASS_NONE) if out of range + return StateClassStrings::get_log_str(static_cast(state_class), 0); } Sensor::Sensor() : state(NAN), raw_state(NAN) {} diff --git a/esphome/components/sensor/sensor.h b/esphome/components/sensor/sensor.h index d9046020f6..f9a45cb1d0 100644 --- a/esphome/components/sensor/sensor.h +++ b/esphome/components/sensor/sensor.h @@ -32,6 +32,7 @@ enum StateClass : uint8_t { STATE_CLASS_TOTAL = 3, STATE_CLASS_MEASUREMENT_ANGLE = 4 }; +constexpr uint8_t STATE_CLASS_LAST = static_cast(STATE_CLASS_MEASUREMENT_ANGLE); const LogString *state_class_to_string(StateClass state_class); diff --git a/esphome/components/sht3xd/sht3xd.cpp b/esphome/components/sht3xd/sht3xd.cpp index d473df43c7..bd3dec6fb8 100644 --- a/esphome/components/sht3xd/sht3xd.cpp +++ b/esphome/components/sht3xd/sht3xd.cpp @@ -72,8 +72,6 @@ void SHT3XDComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHT3XDComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHT3XDComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sht3xd/sht3xd.h b/esphome/components/sht3xd/sht3xd.h index 74f155121b..43f1a4d8e2 100644 --- a/esphome/components/sht3xd/sht3xd.h +++ b/esphome/components/sht3xd/sht3xd.h @@ -17,7 +17,6 @@ class SHT3XDComponent : public PollingComponent, public sensirion_common::Sensir void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_heater_enabled(bool heater_enabled) { heater_enabled_ = heater_enabled; } diff --git a/esphome/components/shtcx/shtcx.cpp b/esphome/components/shtcx/shtcx.cpp index aca305b88d..ec12a5babd 100644 --- a/esphome/components/shtcx/shtcx.cpp +++ b/esphome/components/shtcx/shtcx.cpp @@ -63,8 +63,6 @@ void SHTCXComponent::dump_config() { LOG_SENSOR(" ", "Humidity", this->humidity_sensor_); } -float SHTCXComponent::get_setup_priority() const { return setup_priority::DATA; } - void SHTCXComponent::update() { if (this->status_has_warning()) { ESP_LOGW(TAG, "Retrying communication"); diff --git a/esphome/components/shtcx/shtcx.h b/esphome/components/shtcx/shtcx.h index 084d3bfc35..f9778dce8d 100644 --- a/esphome/components/shtcx/shtcx.h +++ b/esphome/components/shtcx/shtcx.h @@ -17,7 +17,6 @@ class SHTCXComponent : public PollingComponent, public sensirion_common::Sensiri void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void soft_reset(); void sleep(); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index c8dfb4c7bd..1bcb964264 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -44,8 +44,6 @@ void SMT100Component::loop() { } } -float SMT100Component::get_setup_priority() const { return setup_priority::DATA; } - void SMT100Component::dump_config() { ESP_LOGCONFIG(TAG, "SMT100:"); diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index 86827607dc..df8803e1c6 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -17,8 +17,6 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { void loop() override; void update() override; - float get_setup_priority() const override; - void set_counts_sensor(sensor::Sensor *counts_sensor) { this->counts_sensor_ = counts_sensor; } void set_permittivity_sensor(sensor::Sensor *permittivity_sensor) { this->permittivity_sensor_ = permittivity_sensor; diff --git a/esphome/components/sonoff_d1/sonoff_d1.h b/esphome/components/sonoff_d1/sonoff_d1.h index 19ff83f378..20bea23287 100644 --- a/esphome/components/sonoff_d1/sonoff_d1.h +++ b/esphome/components/sonoff_d1/sonoff_d1.h @@ -53,7 +53,6 @@ class SonoffD1Output : public light::LightOutput, public uart::UARTDevice, publi void setup() override{}; void loop() override; void dump_config() override; - float get_setup_priority() const override { return esphome::setup_priority::DATA; } // Custom methods void set_use_rm433_remote(const bool use_rm433_remote) { this->use_rm433_remote_ = use_rm433_remote; } diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 18e1d9782c..10ee6d5212 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -2,7 +2,7 @@ from esphome import automation import esphome.codegen as cg from esphome.components import audio, audio_dac import esphome.config_validation as cv -from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME +from esphome.const import CONF_AUDIO_DAC, CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE, ID from esphome.coroutine import CoroPriority, coroutine_with_priority @@ -11,8 +11,6 @@ CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True -CONF_AUDIO_DAC = "audio_dac" - speaker_ns = cg.esphome_ns.namespace("speaker") Speaker = speaker_ns.class_("Speaker") diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index 370b4576a7..034312236c 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -157,8 +157,14 @@ def _read_audio_file_and_type(file_config): import puremagic - file_type: str = puremagic.from_string(data) - file_type = file_type.removeprefix(".") + try: + file_type: str = puremagic.from_string(data) + file_type = file_type.removeprefix(".") + except puremagic.PureError as e: + raise cv.Invalid( + f"Unable to determine audio file type of '{path}'. " + f"Try re-encoding the file into a supported format. Details: {e}" + ) media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"] if file_type in ("wav"): diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 172bc980a8..94f555c26e 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -519,9 +519,9 @@ void SpeakerMediaPlayer::set_mute_state_(bool mute_state) { if (old_mute_state != mute_state) { if (mute_state) { - this->defer([this]() { this->mute_trigger_->trigger(); }); + this->defer([this]() { this->mute_trigger_.trigger(); }); } else { - this->defer([this]() { this->unmute_trigger_->trigger(); }); + this->defer([this]() { this->unmute_trigger_.trigger(); }); } } } @@ -550,7 +550,7 @@ void SpeakerMediaPlayer::set_volume_(float volume, bool publish) { this->set_mute_state_(false); } - this->defer([this, volume]() { this->volume_trigger_->trigger(volume); }); + this->defer([this, volume]() { this->volume_trigger_.trigger(volume); }); } } // namespace speaker diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 065926d0cf..722f98ceea 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -84,9 +84,9 @@ class SpeakerMediaPlayer : public Component, this->media_format_ = media_format; } - Trigger<> *get_mute_trigger() const { return this->mute_trigger_; } - Trigger<> *get_unmute_trigger() const { return this->unmute_trigger_; } - Trigger *get_volume_trigger() const { return this->volume_trigger_; } + Trigger<> *get_mute_trigger() { return &this->mute_trigger_; } + Trigger<> *get_unmute_trigger() { return &this->unmute_trigger_; } + Trigger *get_volume_trigger() { return &this->volume_trigger_; } void play_file(audio::AudioFile *media_file, bool announcement, bool enqueue); @@ -154,9 +154,9 @@ class SpeakerMediaPlayer : public Component, // Used to save volume/mute state for restoration on reboot ESPPreferenceObject pref_; - Trigger<> *mute_trigger_ = new Trigger<>(); - Trigger<> *unmute_trigger_ = new Trigger<>(); - Trigger *volume_trigger_ = new Trigger(); + Trigger<> mute_trigger_; + Trigger<> unmute_trigger_; + Trigger volume_trigger_; }; } // namespace speaker diff --git a/esphome/components/spi_device/spi_device.cpp b/esphome/components/spi_device/spi_device.cpp index 4cc7286ba9..34f83027db 100644 --- a/esphome/components/spi_device/spi_device.cpp +++ b/esphome/components/spi_device/spi_device.cpp @@ -23,7 +23,5 @@ void SPIDeviceComponent::dump_config() { } } -float SPIDeviceComponent::get_setup_priority() const { return setup_priority::DATA; } - } // namespace spi_device } // namespace esphome diff --git a/esphome/components/spi_device/spi_device.h b/esphome/components/spi_device/spi_device.h index d8aef440a7..e3aa74aaf0 100644 --- a/esphome/components/spi_device/spi_device.h +++ b/esphome/components/spi_device/spi_device.h @@ -13,8 +13,6 @@ class SPIDeviceComponent : public Component, void setup() override; void dump_config() override; - float get_setup_priority() const override; - protected: }; diff --git a/esphome/components/sprinkler/sprinkler.cpp b/esphome/components/sprinkler/sprinkler.cpp index 2a60eb042b..eae6ecbf31 100644 --- a/esphome/components/sprinkler/sprinkler.cpp +++ b/esphome/components/sprinkler/sprinkler.cpp @@ -29,7 +29,7 @@ void SprinklerControllerNumber::setup() { } void SprinklerControllerNumber::control(float value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); this->publish_state(value); @@ -39,8 +39,7 @@ void SprinklerControllerNumber::control(float value) { void SprinklerControllerNumber::dump_config() { LOG_NUMBER("", "Sprinkler Controller Number", this); } -SprinklerControllerSwitch::SprinklerControllerSwitch() - : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} +SprinklerControllerSwitch::SprinklerControllerSwitch() = default; void SprinklerControllerSwitch::loop() { // Loop is only enabled when f_ has a value (see setup()) @@ -56,11 +55,11 @@ void SprinklerControllerSwitch::write_state(bool state) { } if (state) { - this->prev_trigger_ = this->turn_on_trigger_; - this->turn_on_trigger_->trigger(); + this->prev_trigger_ = &this->turn_on_trigger_; + this->turn_on_trigger_.trigger(); } else { - this->prev_trigger_ = this->turn_off_trigger_; - this->turn_off_trigger_->trigger(); + this->prev_trigger_ = &this->turn_off_trigger_; + this->turn_off_trigger_.trigger(); } this->publish_state(state); @@ -69,9 +68,6 @@ void SprinklerControllerSwitch::write_state(bool state) { void SprinklerControllerSwitch::set_state_lambda(std::function()> &&f) { this->f_ = f; } float SprinklerControllerSwitch::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *SprinklerControllerSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } -Trigger<> *SprinklerControllerSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } - void SprinklerControllerSwitch::setup() { this->state = this->get_initial_state_with_restore_mode().value_or(false); // Disable loop if no state lambda is set - nothing to poll diff --git a/esphome/components/sprinkler/sprinkler.h b/esphome/components/sprinkler/sprinkler.h index 04efa28031..a3cdef5b1a 100644 --- a/esphome/components/sprinkler/sprinkler.h +++ b/esphome/components/sprinkler/sprinkler.h @@ -76,7 +76,7 @@ class SprinklerControllerNumber : public number::Number, public Component { void dump_config() override; float get_setup_priority() const override { return setup_priority::PROCESSOR; } - Trigger *get_set_trigger() const { return set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } @@ -84,7 +84,7 @@ class SprinklerControllerNumber : public number::Number, public Component { void control(float value) override; float initial_value_{NAN}; bool restore_value_{true}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; ESPPreferenceObject pref_; }; @@ -97,8 +97,8 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void dump_config() override; void set_state_lambda(std::function()> &&f); - Trigger<> *get_turn_on_trigger() const; - Trigger<> *get_turn_off_trigger() const; + Trigger<> *get_turn_on_trigger() { return &this->turn_on_trigger_; } + Trigger<> *get_turn_off_trigger() { return &this->turn_off_trigger_; } void loop() override; float get_setup_priority() const override; @@ -107,8 +107,8 @@ class SprinklerControllerSwitch : public switch_::Switch, public Component { void write_state(bool state) override; optional()>> f_; - Trigger<> *turn_on_trigger_; - Trigger<> *turn_off_trigger_; + Trigger<> turn_on_trigger_; + Trigger<> turn_off_trigger_; Trigger<> *prev_trigger_{nullptr}; }; diff --git a/esphome/components/sts3x/sts3x.cpp b/esphome/components/sts3x/sts3x.cpp index eee2aca73e..8713b0b6b8 100644 --- a/esphome/components/sts3x/sts3x.cpp +++ b/esphome/components/sts3x/sts3x.cpp @@ -41,7 +41,7 @@ void STS3XComponent::dump_config() { LOG_SENSOR(" ", "STS3x", this); } -float STS3XComponent::get_setup_priority() const { return setup_priority::DATA; } + void STS3XComponent::update() { if (this->status_has_warning()) { ESP_LOGD(TAG, "Retrying to reconnect the sensor."); diff --git a/esphome/components/sts3x/sts3x.h b/esphome/components/sts3x/sts3x.h index 8f806a3471..6c1dd2b244 100644 --- a/esphome/components/sts3x/sts3x.h +++ b/esphome/components/sts3x/sts3x.h @@ -14,7 +14,6 @@ class STS3XComponent : public sensor::Sensor, public PollingComponent, public se public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; }; diff --git a/esphome/components/sx126x/__init__.py b/esphome/components/sx126x/__init__.py index ed878ed0d4..413eb139d6 100644 --- a/esphome/components/sx126x/__init__.py +++ b/esphome/components/sx126x/__init__.py @@ -213,7 +213,7 @@ CONFIG_SCHEMA = ( cv.Optional(CONF_ON_PACKET): automation.validate_automation(single=True), cv.Optional(CONF_PA_POWER, default=17): cv.int_range(min=-3, max=22), cv.Optional(CONF_PA_RAMP, default="40us"): cv.enum(RAMP), - cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=256), + cv.Optional(CONF_PAYLOAD_LENGTH, default=0): cv.int_range(min=0, max=255), cv.Optional(CONF_PREAMBLE_DETECT, default=2): cv.int_range(min=0, max=4), cv.Optional(CONF_PREAMBLE_SIZE, default=8): cv.int_range(min=1, max=65535), cv.Required(CONF_RST_PIN): pins.gpio_output_pin_schema, diff --git a/esphome/components/sx126x/sx126x.cpp b/esphome/components/sx126x/sx126x.cpp index 707d6f1fbf..64cd24b171 100644 --- a/esphome/components/sx126x/sx126x.cpp +++ b/esphome/components/sx126x/sx126x.cpp @@ -343,7 +343,7 @@ void SX126x::call_listeners_(const std::vector &packet, float rssi, flo for (auto &listener : this->listeners_) { listener->on_packet(packet, rssi, snr); } - this->packet_trigger_->trigger(packet, rssi, snr); + this->packet_trigger_.trigger(packet, rssi, snr); } void SX126x::loop() { diff --git a/esphome/components/sx126x/sx126x.h b/esphome/components/sx126x/sx126x.h index 850d7d4c77..a758d63795 100644 --- a/esphome/components/sx126x/sx126x.h +++ b/esphome/components/sx126x/sx126x.h @@ -97,7 +97,7 @@ class SX126x : public Component, void configure(); SX126xError transmit_packet(const std::vector &packet); void register_listener(SX126xListener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + Trigger, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: void configure_fsk_ook_(); @@ -111,7 +111,7 @@ class SX126x : public Component, void read_register_(uint16_t reg, uint8_t *data, uint8_t size); void call_listeners_(const std::vector &packet, float rssi, float snr); void wait_busy_(); - Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + Trigger, float, float> packet_trigger_; std::vector listeners_; std::vector packet_; std::vector sync_value_; diff --git a/esphome/components/sx127x/sx127x.cpp b/esphome/components/sx127x/sx127x.cpp index 3185574b1a..caf68b6d51 100644 --- a/esphome/components/sx127x/sx127x.cpp +++ b/esphome/components/sx127x/sx127x.cpp @@ -300,7 +300,7 @@ void SX127x::call_listeners_(const std::vector &packet, float rssi, flo for (auto &listener : this->listeners_) { listener->on_packet(packet, rssi, snr); } - this->packet_trigger_->trigger(packet, rssi, snr); + this->packet_trigger_.trigger(packet, rssi, snr); } void SX127x::loop() { diff --git a/esphome/components/sx127x/sx127x.h b/esphome/components/sx127x/sx127x.h index 0600b51201..be7b6d8d9f 100644 --- a/esphome/components/sx127x/sx127x.h +++ b/esphome/components/sx127x/sx127x.h @@ -83,7 +83,7 @@ class SX127x : public Component, void configure(); SX127xError transmit_packet(const std::vector &packet); void register_listener(SX127xListener *listener) { this->listeners_.push_back(listener); } - Trigger, float, float> *get_packet_trigger() const { return this->packet_trigger_; }; + Trigger, float, float> *get_packet_trigger() { return &this->packet_trigger_; } protected: void configure_fsk_ook_(); @@ -94,7 +94,7 @@ class SX127x : public Component, void write_register_(uint8_t reg, uint8_t value); void call_listeners_(const std::vector &packet, float rssi, float snr); uint8_t read_register_(uint8_t reg); - Trigger, float, float> *packet_trigger_{new Trigger, float, float>()}; + Trigger, float, float> packet_trigger_; std::vector listeners_; std::vector packet_; std::vector sync_value_; diff --git a/esphome/components/sy6970/sy6970.h b/esphome/components/sy6970/sy6970.h index bacc072f9b..2225dd781b 100644 --- a/esphome/components/sy6970/sy6970.h +++ b/esphome/components/sy6970/sy6970.h @@ -87,7 +87,6 @@ class SY6970Component : public PollingComponent, public i2c::I2CDevice { void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } // Listener registration void add_listener(SY6970Listener *listener) { this->listeners_.push_back(listener); } diff --git a/esphome/components/t6615/t6615.cpp b/esphome/components/t6615/t6615.cpp index c2ac88ee2e..75f9ed108e 100644 --- a/esphome/components/t6615/t6615.cpp +++ b/esphome/components/t6615/t6615.cpp @@ -86,7 +86,6 @@ void T6615Component::query_ppm_() { this->send_ppm_command_(); } -float T6615Component::get_setup_priority() const { return setup_priority::DATA; } void T6615Component::dump_config() { ESP_LOGCONFIG(TAG, "T6615:"); LOG_SENSOR(" ", "CO2", this->co2_sensor_); diff --git a/esphome/components/t6615/t6615.h b/esphome/components/t6615/t6615.h index fb53032e8d..69c406a5ba 100644 --- a/esphome/components/t6615/t6615.h +++ b/esphome/components/t6615/t6615.h @@ -22,8 +22,6 @@ enum class T6615Command : uint8_t { class T6615Component : public PollingComponent, public uart::UARTDevice { public: - float get_setup_priority() const override; - void loop() override; void update() override; void dump_config() override; diff --git a/esphome/components/tc74/tc74.cpp b/esphome/components/tc74/tc74.cpp index abf3839e00..969ef3671e 100644 --- a/esphome/components/tc74/tc74.cpp +++ b/esphome/components/tc74/tc74.cpp @@ -61,7 +61,5 @@ void TC74Component::read_temperature_() { this->status_clear_warning(); } -float TC74Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tc74 } // namespace esphome diff --git a/esphome/components/tc74/tc74.h b/esphome/components/tc74/tc74.h index 5d70c05420..f3ce225ff4 100644 --- a/esphome/components/tc74/tc74.h +++ b/esphome/components/tc74/tc74.h @@ -15,8 +15,6 @@ class TC74Component : public PollingComponent, public i2c::I2CDevice, public sen /// Update the sensor value (temperature). void update() override; - float get_setup_priority() const override; - protected: /// Internal method to read the temperature from the component after it has been scheduled. void read_temperature_(); diff --git a/esphome/components/tcs34725/tcs34725.cpp b/esphome/components/tcs34725/tcs34725.cpp index e4e5547595..4fe87de0ca 100644 --- a/esphome/components/tcs34725/tcs34725.cpp +++ b/esphome/components/tcs34725/tcs34725.cpp @@ -56,7 +56,6 @@ void TCS34725Component::dump_config() { LOG_SENSOR(" ", "Illuminance", this->illuminance_sensor_); LOG_SENSOR(" ", "Color Temperature", this->color_temperature_sensor_); } -float TCS34725Component::get_setup_priority() const { return setup_priority::DATA; } /*! * @brief Converts the raw R/G/B values to color temperature in degrees diff --git a/esphome/components/tcs34725/tcs34725.h b/esphome/components/tcs34725/tcs34725.h index 23985e8221..85bb383e4b 100644 --- a/esphome/components/tcs34725/tcs34725.h +++ b/esphome/components/tcs34725/tcs34725.h @@ -52,7 +52,6 @@ class TCS34725Component : public PollingComponent, public i2c::I2CDevice { } void setup() override; - float get_setup_priority() const override; void update() override; void dump_config() override; diff --git a/esphome/components/tee501/tee501.cpp b/esphome/components/tee501/tee501.cpp index 06481b628b..00a62247f9 100644 --- a/esphome/components/tee501/tee501.cpp +++ b/esphome/components/tee501/tee501.cpp @@ -43,7 +43,6 @@ void TEE501Component::dump_config() { LOG_SENSOR(" ", "TEE501", this); } -float TEE501Component::get_setup_priority() const { return setup_priority::DATA; } void TEE501Component::update() { uint8_t address_1[] = {0x2C, 0x1B}; this->write(address_1, 2); diff --git a/esphome/components/tee501/tee501.h b/esphome/components/tee501/tee501.h index 2437ac92eb..62a6f1c944 100644 --- a/esphome/components/tee501/tee501.h +++ b/esphome/components/tee501/tee501.h @@ -12,7 +12,6 @@ class TEE501Component : public sensor::Sensor, public PollingComponent, public i public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; protected: diff --git a/esphome/components/tem3200/tem3200.cpp b/esphome/components/tem3200/tem3200.cpp index b31496142c..9c305f8f6f 100644 --- a/esphome/components/tem3200/tem3200.cpp +++ b/esphome/components/tem3200/tem3200.cpp @@ -51,8 +51,6 @@ void TEM3200Component::dump_config() { LOG_SENSOR(" ", "Temperature", this->temperature_sensor_); } -float TEM3200Component::get_setup_priority() const { return setup_priority::DATA; } - i2c::ErrorCode TEM3200Component::read_(uint8_t &status, uint16_t &raw_temperature, uint16_t &raw_pressure) { uint8_t response[4] = {0x00, 0x00, 0x00, 0x00}; diff --git a/esphome/components/tem3200/tem3200.h b/esphome/components/tem3200/tem3200.h index c84a2aba21..37589b2a06 100644 --- a/esphome/components/tem3200/tem3200.h +++ b/esphome/components/tem3200/tem3200.h @@ -15,7 +15,6 @@ class TEM3200Component : public PollingComponent, public i2c::I2CDevice { this->raw_pressure_sensor_ = raw_pressure_sensor; } - float get_setup_priority() const override; void setup() override; void dump_config() override; void update() override; diff --git a/esphome/components/template/cover/template_cover.cpp b/esphome/components/template/cover/template_cover.cpp index 9c8a8fc9bc..7f5d68623f 100644 --- a/esphome/components/template/cover/template_cover.cpp +++ b/esphome/components/template/cover/template_cover.cpp @@ -7,13 +7,7 @@ using namespace esphome::cover; static const char *const TAG = "template.cover"; -TemplateCover::TemplateCover() - : open_trigger_(new Trigger<>()), - close_trigger_(new Trigger<>), - stop_trigger_(new Trigger<>()), - toggle_trigger_(new Trigger<>()), - position_trigger_(new Trigger()), - tilt_trigger_(new Trigger()) {} +TemplateCover::TemplateCover() = default; void TemplateCover::setup() { switch (this->restore_mode_) { case COVER_NO_RESTORE: @@ -62,22 +56,22 @@ void TemplateCover::loop() { void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } -Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } -Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } -Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } +Trigger<> *TemplateCover::get_open_trigger() { return &this->open_trigger_; } +Trigger<> *TemplateCover::get_close_trigger() { return &this->close_trigger_; } +Trigger<> *TemplateCover::get_stop_trigger() { return &this->stop_trigger_; } +Trigger<> *TemplateCover::get_toggle_trigger() { return &this->toggle_trigger_; } void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } void TemplateCover::control(const CoverCall &call) { if (call.get_stop()) { this->stop_prev_trigger_(); - this->stop_trigger_->trigger(); - this->prev_command_trigger_ = this->stop_trigger_; + this->stop_trigger_.trigger(); + this->prev_command_trigger_ = &this->stop_trigger_; this->publish_state(); } if (call.get_toggle().has_value()) { this->stop_prev_trigger_(); - this->toggle_trigger_->trigger(); - this->prev_command_trigger_ = this->toggle_trigger_; + this->toggle_trigger_.trigger(); + this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } if (call.get_position().has_value()) { @@ -85,13 +79,13 @@ void TemplateCover::control(const CoverCall &call) { this->stop_prev_trigger_(); if (pos == COVER_OPEN) { - this->open_trigger_->trigger(); - this->prev_command_trigger_ = this->open_trigger_; + this->open_trigger_.trigger(); + this->prev_command_trigger_ = &this->open_trigger_; } else if (pos == COVER_CLOSED) { - this->close_trigger_->trigger(); - this->prev_command_trigger_ = this->close_trigger_; + this->close_trigger_.trigger(); + this->prev_command_trigger_ = &this->close_trigger_; } else { - this->position_trigger_->trigger(pos); + this->position_trigger_.trigger(pos); } if (this->optimistic_) { @@ -101,7 +95,7 @@ void TemplateCover::control(const CoverCall &call) { if (call.get_tilt().has_value()) { auto tilt = *call.get_tilt(); - this->tilt_trigger_->trigger(tilt); + this->tilt_trigger_.trigger(tilt); if (this->optimistic_) { this->tilt = tilt; @@ -119,8 +113,8 @@ CoverTraits TemplateCover::get_traits() { traits.set_supports_tilt(this->has_tilt_); return traits; } -Trigger *TemplateCover::get_position_trigger() const { return this->position_trigger_; } -Trigger *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } +Trigger *TemplateCover::get_position_trigger() { return &this->position_trigger_; } +Trigger *TemplateCover::get_tilt_trigger() { return &this->tilt_trigger_; } void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } diff --git a/esphome/components/template/cover/template_cover.h b/esphome/components/template/cover/template_cover.h index 9c4a787283..20c092cda7 100644 --- a/esphome/components/template/cover/template_cover.h +++ b/esphome/components/template/cover/template_cover.h @@ -19,12 +19,12 @@ class TemplateCover final : public cover::Cover, public Component { template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } template void set_tilt_lambda(F &&f) { this->tilt_f_.set(std::forward(f)); } - Trigger<> *get_open_trigger() const; - Trigger<> *get_close_trigger() const; - Trigger<> *get_stop_trigger() const; - Trigger<> *get_toggle_trigger() const; - Trigger *get_position_trigger() const; - Trigger *get_tilt_trigger() const; + Trigger<> *get_open_trigger(); + Trigger<> *get_close_trigger(); + Trigger<> *get_stop_trigger(); + Trigger<> *get_toggle_trigger(); + Trigger *get_position_trigger(); + Trigger *get_tilt_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); @@ -49,16 +49,16 @@ class TemplateCover final : public cover::Cover, public Component { TemplateLambda tilt_f_; bool assumed_state_{false}; bool optimistic_{false}; - Trigger<> *open_trigger_; - Trigger<> *close_trigger_; + Trigger<> open_trigger_; + Trigger<> close_trigger_; bool has_stop_{false}; bool has_toggle_{false}; - Trigger<> *stop_trigger_; - Trigger<> *toggle_trigger_; + Trigger<> stop_trigger_; + Trigger<> toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; - Trigger *position_trigger_; + Trigger position_trigger_; bool has_position_{false}; - Trigger *tilt_trigger_; + Trigger tilt_trigger_; bool has_tilt_{false}; }; diff --git a/esphome/components/template/datetime/template_date.cpp b/esphome/components/template/datetime/template_date.cpp index be1d875a7e..8a5f11b876 100644 --- a/esphome/components/template/datetime/template_date.cpp +++ b/esphome/components/template/datetime/template_date.cpp @@ -62,7 +62,7 @@ void TemplateDate::control(const datetime::DateCall &call) { if (has_day) value.day_of_month = *call.get_day(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) diff --git a/esphome/components/template/datetime/template_date.h b/esphome/components/template/datetime/template_date.h index 0379a9bc67..acf823a34d 100644 --- a/esphome/components/template/datetime/template_date.h +++ b/esphome/components/template/datetime/template_date.h @@ -22,7 +22,7 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateDate final : public datetime::DateEntity, public PollingComponent bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/datetime/template_datetime.cpp b/esphome/components/template/datetime/template_datetime.cpp index e134f2b654..269a1d06ca 100644 --- a/esphome/components/template/datetime/template_datetime.cpp +++ b/esphome/components/template/datetime/template_datetime.cpp @@ -80,7 +80,7 @@ void TemplateDateTime::control(const datetime::DateTimeCall &call) { if (has_second) value.second = *call.get_second(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_year) diff --git a/esphome/components/template/datetime/template_datetime.h b/esphome/components/template/datetime/template_datetime.h index b7eb490933..575065a3dd 100644 --- a/esphome/components/template/datetime/template_datetime.h +++ b/esphome/components/template/datetime/template_datetime.h @@ -22,7 +22,7 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateDateTime final : public datetime::DateTimeEntity, public PollingCo bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/datetime/template_time.cpp b/esphome/components/template/datetime/template_time.cpp index 586e126e3b..9c81687116 100644 --- a/esphome/components/template/datetime/template_time.cpp +++ b/esphome/components/template/datetime/template_time.cpp @@ -62,7 +62,7 @@ void TemplateTime::control(const datetime::TimeCall &call) { if (has_second) value.second = *call.get_second(); - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) { if (has_hour) diff --git a/esphome/components/template/datetime/template_time.h b/esphome/components/template/datetime/template_time.h index cb83b1b3e5..924b53cc71 100644 --- a/esphome/components/template/datetime/template_time.h +++ b/esphome/components/template/datetime/template_time.h @@ -22,7 +22,7 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(ESPTime initial_value) { this->initial_value_ = initial_value; } @@ -34,7 +34,7 @@ class TemplateTime final : public datetime::TimeEntity, public PollingComponent bool optimistic_{false}; ESPTime initial_value_{}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/lock/template_lock.cpp b/esphome/components/template/lock/template_lock.cpp index de8f9b762c..dbc4501ce7 100644 --- a/esphome/components/template/lock/template_lock.cpp +++ b/esphome/components/template/lock/template_lock.cpp @@ -7,8 +7,7 @@ using namespace esphome::lock; static const char *const TAG = "template.lock"; -TemplateLock::TemplateLock() - : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} +TemplateLock::TemplateLock() = default; void TemplateLock::setup() { if (!this->f_.has_value()) @@ -28,11 +27,11 @@ void TemplateLock::control(const lock::LockCall &call) { auto state = *call.get_state(); if (state == LOCK_STATE_LOCKED) { - this->prev_trigger_ = this->lock_trigger_; - this->lock_trigger_->trigger(); + this->prev_trigger_ = &this->lock_trigger_; + this->lock_trigger_.trigger(); } else if (state == LOCK_STATE_UNLOCKED) { - this->prev_trigger_ = this->unlock_trigger_; - this->unlock_trigger_->trigger(); + this->prev_trigger_ = &this->unlock_trigger_; + this->unlock_trigger_.trigger(); } if (this->optimistic_) @@ -42,14 +41,11 @@ void TemplateLock::open_latch() { if (this->prev_trigger_ != nullptr) { this->prev_trigger_->stop_action(); } - this->prev_trigger_ = this->open_trigger_; - this->open_trigger_->trigger(); + this->prev_trigger_ = &this->open_trigger_; + this->open_trigger_.trigger(); } void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } -Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } -Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; } void TemplateLock::dump_config() { LOG_LOCK("", "Template Lock", this); ESP_LOGCONFIG(TAG, " Optimistic: %s", YESNO(this->optimistic_)); diff --git a/esphome/components/template/lock/template_lock.h b/esphome/components/template/lock/template_lock.h index f4396c2c5d..03e3e86d88 100644 --- a/esphome/components/template/lock/template_lock.h +++ b/esphome/components/template/lock/template_lock.h @@ -15,9 +15,9 @@ class TemplateLock final : public lock::Lock, public Component { void dump_config() override; template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } - Trigger<> *get_lock_trigger() const; - Trigger<> *get_unlock_trigger() const; - Trigger<> *get_open_trigger() const; + Trigger<> *get_lock_trigger() { return &this->lock_trigger_; } + Trigger<> *get_unlock_trigger() { return &this->unlock_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } void set_optimistic(bool optimistic); void loop() override; @@ -29,9 +29,9 @@ class TemplateLock final : public lock::Lock, public Component { TemplateLambda f_; bool optimistic_{false}; - Trigger<> *lock_trigger_; - Trigger<> *unlock_trigger_; - Trigger<> *open_trigger_; + Trigger<> lock_trigger_; + Trigger<> unlock_trigger_; + Trigger<> open_trigger_; Trigger<> *prev_trigger_{nullptr}; }; diff --git a/esphome/components/template/number/template_number.cpp b/esphome/components/template/number/template_number.cpp index 885265cf5d..64c2deb281 100644 --- a/esphome/components/template/number/template_number.cpp +++ b/esphome/components/template/number/template_number.cpp @@ -36,7 +36,7 @@ void TemplateNumber::update() { } void TemplateNumber::control(float value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) this->publish_state(value); diff --git a/esphome/components/template/number/template_number.h b/esphome/components/template/number/template_number.h index 42c27fc3ca..e51e858ccf 100644 --- a/esphome/components/template/number/template_number.h +++ b/esphome/components/template/number/template_number.h @@ -17,7 +17,7 @@ class TemplateNumber final : public number::Number, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { optimistic_ = optimistic; } void set_initial_value(float initial_value) { initial_value_ = initial_value; } void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } @@ -27,7 +27,7 @@ class TemplateNumber final : public number::Number, public PollingComponent { bool optimistic_{false}; float initial_value_{NAN}; bool restore_value_{false}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_; ESPPreferenceObject pref_; diff --git a/esphome/components/template/output/template_output.h b/esphome/components/template/output/template_output.h index e536660b02..6fe8e53855 100644 --- a/esphome/components/template/output/template_output.h +++ b/esphome/components/template/output/template_output.h @@ -8,22 +8,22 @@ namespace esphome::template_ { class TemplateBinaryOutput final : public output::BinaryOutput { public: - Trigger *get_trigger() const { return trigger_; } + Trigger *get_trigger() { return &this->trigger_; } protected: - void write_state(bool state) override { this->trigger_->trigger(state); } + void write_state(bool state) override { this->trigger_.trigger(state); } - Trigger *trigger_ = new Trigger(); + Trigger trigger_; }; class TemplateFloatOutput final : public output::FloatOutput { public: - Trigger *get_trigger() const { return trigger_; } + Trigger *get_trigger() { return &this->trigger_; } protected: - void write_state(float state) override { this->trigger_->trigger(state); } + void write_state(float state) override { this->trigger_.trigger(state); } - Trigger *trigger_ = new Trigger(); + Trigger trigger_; }; } // namespace esphome::template_ diff --git a/esphome/components/template/select/__init__.py b/esphome/components/template/select/__init__.py index 574f1f5fb7..8de0b65c2d 100644 --- a/esphome/components/template/select/__init__.py +++ b/esphome/components/template/select/__init__.py @@ -10,35 +10,65 @@ from esphome.const import ( CONF_OPTIONS, CONF_RESTORE_VALUE, CONF_SET_ACTION, + CONF_UPDATE_INTERVAL, + SCHEDULER_DONT_RUN, ) +from esphome.core import TimePeriodMilliseconds +from esphome.cpp_generator import TemplateArguments from .. import template_ns TemplateSelect = template_ns.class_( "TemplateSelect", select.Select, cg.PollingComponent ) +TemplateSelectWithSetAction = template_ns.class_( + "TemplateSelectWithSetAction", TemplateSelect +) def validate(config): + errors = [] if CONF_LAMBDA in config: if config[CONF_OPTIMISTIC]: - raise cv.Invalid("optimistic cannot be used with lambda") + errors.append( + cv.Invalid( + "optimistic cannot be used with lambda", path=[CONF_OPTIMISTIC] + ) + ) if CONF_INITIAL_OPTION in config: - raise cv.Invalid("initial_value cannot be used with lambda") + errors.append( + cv.Invalid( + "initial_value cannot be used with lambda", + path=[CONF_INITIAL_OPTION], + ) + ) if CONF_RESTORE_VALUE in config: - raise cv.Invalid("restore_value cannot be used with lambda") + errors.append( + cv.Invalid( + "restore_value cannot be used with lambda", + path=[CONF_RESTORE_VALUE], + ) + ) elif CONF_INITIAL_OPTION in config: if config[CONF_INITIAL_OPTION] not in config[CONF_OPTIONS]: - raise cv.Invalid( - f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]" + errors.append( + cv.Invalid( + f"initial_option '{config[CONF_INITIAL_OPTION]}' is not a valid option [{', '.join(config[CONF_OPTIONS])}]", + path=[CONF_INITIAL_OPTION], + ) ) else: config[CONF_INITIAL_OPTION] = config[CONF_OPTIONS][0] if not config[CONF_OPTIMISTIC] and CONF_SET_ACTION not in config: - raise cv.Invalid( - "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + errors.append( + cv.Invalid( + "Either optimistic mode must be enabled, or set_action must be set, to handle the option being set." + ) ) + if errors: + raise cv.MultipleInvalid(errors) + return config @@ -62,29 +92,34 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): - var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) - await select.register_select(var, config, options=config[CONF_OPTIONS]) + var_id = config[CONF_ID] + if CONF_SET_ACTION in config: + var_id.type = TemplateSelectWithSetAction + has_lambda = CONF_LAMBDA in config + optimistic = config.get(CONF_OPTIMISTIC, False) + restore_value = config.get(CONF_RESTORE_VALUE, False) + options = config[CONF_OPTIONS] + initial_option = config.get(CONF_INITIAL_OPTION, 0) + initial_option_index = options.index(initial_option) if not has_lambda else 0 + + var = cg.new_Pvariable( + var_id, + TemplateArguments(has_lambda, optimistic, restore_value, initial_option_index), + ) + component_config = config.copy() + if not has_lambda: + # No point in polling if not using a lambda + component_config[CONF_UPDATE_INTERVAL] = TimePeriodMilliseconds( + milliseconds=SCHEDULER_DONT_RUN + ) + await cg.register_component(var, component_config) + await select.register_select(var, config, options=options) if CONF_LAMBDA in config: - template_ = await cg.process_lambda( + lambda_ = await cg.process_lambda( config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.std_string) ) - cg.add(var.set_template(template_)) - - else: - # Only set if non-default to avoid bloating setup() function - if config[CONF_OPTIMISTIC]: - cg.add(var.set_optimistic(True)) - initial_option_index = config[CONF_OPTIONS].index(config[CONF_INITIAL_OPTION]) - # Only set if non-zero to avoid bloating setup() function - # (initial_option_index_ is zero-initialized in the header) - if initial_option_index != 0: - cg.add(var.set_initial_option_index(initial_option_index)) - - # Only set if True (default is False) - if config.get(CONF_RESTORE_VALUE): - cg.add(var.set_restore_value(True)) + cg.add(var.set_lambda(lambda_)) if CONF_SET_ACTION in config: await automation.build_automation( diff --git a/esphome/components/template/select/template_select.cpp b/esphome/components/template/select/template_select.cpp index fa34aa9fa7..e68729c2d4 100644 --- a/esphome/components/template/select/template_select.cpp +++ b/esphome/components/template/select/template_select.cpp @@ -5,61 +5,44 @@ namespace esphome::template_ { static const char *const TAG = "template.select"; -void TemplateSelect::setup() { - if (this->f_.has_value()) - return; - - size_t index = this->initial_option_index_; - if (this->restore_value_) { - this->pref_ = this->make_entity_preference(); - size_t restored_index; - if (this->pref_.load(&restored_index) && this->has_index(restored_index)) { - index = restored_index; - ESP_LOGD(TAG, "State from restore: %s", this->option_at(index)); - } else { - ESP_LOGD(TAG, "State from initial (could not load or invalid stored index): %s", this->option_at(index)); - } +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, + const size_t initial_option_index, bool restore_value) { + LOG_SELECT("", "Template Select", sel_comp); + if (has_lambda) { + LOG_UPDATE_INTERVAL(sel_comp); } else { - ESP_LOGD(TAG, "State from initial: %s", this->option_at(index)); + ESP_LOGCONFIG(TAG, + " Optimistic: %s\n" + " Initial Option: %s\n" + " Restore Value: %s", + YESNO(optimistic), sel_comp->option_at(initial_option_index), YESNO(restore_value)); } - - this->publish_state(index); } -void TemplateSelect::update() { - if (!this->f_.has_value()) - return; +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index) { + ESP_LOGD(TAG, "State from initial: %s", sel_comp->option_at(initial_index)); + sel_comp->publish_state(initial_index); +} - auto val = this->f_(); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index) { + size_t index = initial_index; + if (pref.load(&index) && sel_comp->has_index(index)) { + ESP_LOGD(TAG, "State from restore: %s", sel_comp->option_at(index)); + } else { + index = initial_index; + ESP_LOGD(TAG, "State from initial (no valid stored index): %s", sel_comp->option_at(initial_index)); + } + sel_comp->publish_state(index); +} + +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val) { if (val.has_value()) { - if (!this->has_option(*val)) { + if (!sel_comp->has_option(*val)) { ESP_LOGE(TAG, "Lambda returned an invalid option: %s", (*val).c_str()); return; } - this->publish_state(*val); + sel_comp->publish_state(*val); } } -void TemplateSelect::control(size_t index) { - this->set_trigger_->trigger(StringRef(this->option_at(index))); - - if (this->optimistic_) - this->publish_state(index); - - if (this->restore_value_) - this->pref_.save(&index); -} - -void TemplateSelect::dump_config() { - LOG_SELECT("", "Template Select", this); - LOG_UPDATE_INTERVAL(this); - if (this->f_.has_value()) - return; - ESP_LOGCONFIG(TAG, - " Optimistic: %s\n" - " Initial Option: %s\n" - " Restore Value: %s", - YESNO(this->optimistic_), this->option_at(this->initial_option_index_), YESNO(this->restore_value_)); -} - } // namespace esphome::template_ diff --git a/esphome/components/template/select/template_select.h b/esphome/components/template/select/template_select.h index 114d25b9ce..5da6d732bd 100644 --- a/esphome/components/template/select/template_select.h +++ b/esphome/components/template/select/template_select.h @@ -9,29 +9,71 @@ namespace esphome::template_ { -class TemplateSelect final : public select::Select, public PollingComponent { - public: - template void set_template(F &&f) { this->f_.set(std::forward(f)); } +struct Empty {}; +class BaseTemplateSelect : public select::Select, public PollingComponent {}; - void setup() override; - void update() override; - void dump_config() override; +void dump_config_helper(BaseTemplateSelect *sel_comp, bool optimistic, bool has_lambda, size_t initial_option_index, + bool restore_value); +void setup_initial(BaseTemplateSelect *sel_comp, size_t initial_index); +void setup_with_restore(BaseTemplateSelect *sel_comp, ESPPreferenceObject &pref, size_t initial_index); +void update_lambda(BaseTemplateSelect *sel_comp, const optional &val); + +/// Base template select class - used when no set_action is configured + +template +class TemplateSelect : public BaseTemplateSelect { + public: + template void set_lambda(F &&f) { + if constexpr (HAS_LAMBDA) { + this->f_.set(std::forward(f)); + } + } + + void setup() override { + if constexpr (!HAS_LAMBDA) { + if constexpr (RESTORE_VALUE) { + this->pref_ = this->template make_entity_preference(); + setup_with_restore(this, this->pref_, INITIAL_OPTION_INDEX); + } else { + setup_initial(this, INITIAL_OPTION_INDEX); + } + } + } + + void update() override { + if constexpr (HAS_LAMBDA) { + update_lambda(this, this->f_()); + } + } + void dump_config() override { + dump_config_helper(this, OPTIMISTIC, HAS_LAMBDA, INITIAL_OPTION_INDEX, RESTORE_VALUE); + }; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } - void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } - void set_initial_option_index(size_t initial_option_index) { this->initial_option_index_ = initial_option_index; } - void set_restore_value(bool restore_value) { this->restore_value_ = restore_value; } + protected: + void control(size_t index) override { + if constexpr (OPTIMISTIC) + this->publish_state(index); + if constexpr (RESTORE_VALUE) + this->pref_.save(&index); + } + [[no_unique_address]] std::conditional_t, Empty> f_{}; + [[no_unique_address]] std::conditional_t pref_{}; +}; + +/// Template select with set_action trigger - only instantiated when set_action is configured +template +class TemplateSelectWithSetAction final + : public TemplateSelect { + public: + Trigger *get_set_trigger() { return &this->set_trigger_; } protected: - void control(size_t index) override; - bool optimistic_ = false; - size_t initial_option_index_{0}; - bool restore_value_ = false; - Trigger *set_trigger_ = new Trigger(); - TemplateLambda f_; - - ESPPreferenceObject pref_; + void control(size_t index) override { + this->set_trigger_.trigger(StringRef(this->option_at(index))); + TemplateSelect::control(index); + } + Trigger set_trigger_; }; } // namespace esphome::template_ diff --git a/esphome/components/template/switch/template_switch.cpp b/esphome/components/template/switch/template_switch.cpp index cfa8798e75..05288b2d4e 100644 --- a/esphome/components/template/switch/template_switch.cpp +++ b/esphome/components/template/switch/template_switch.cpp @@ -5,7 +5,7 @@ namespace esphome::template_ { static const char *const TAG = "template.switch"; -TemplateSwitch::TemplateSwitch() : turn_on_trigger_(new Trigger<>()), turn_off_trigger_(new Trigger<>()) {} +TemplateSwitch::TemplateSwitch() = default; void TemplateSwitch::loop() { auto s = this->f_(); @@ -19,11 +19,11 @@ void TemplateSwitch::write_state(bool state) { } if (state) { - this->prev_trigger_ = this->turn_on_trigger_; - this->turn_on_trigger_->trigger(); + this->prev_trigger_ = &this->turn_on_trigger_; + this->turn_on_trigger_.trigger(); } else { - this->prev_trigger_ = this->turn_off_trigger_; - this->turn_off_trigger_->trigger(); + this->prev_trigger_ = &this->turn_off_trigger_; + this->turn_off_trigger_.trigger(); } if (this->optimistic_) @@ -32,8 +32,8 @@ void TemplateSwitch::write_state(bool state) { void TemplateSwitch::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } bool TemplateSwitch::assumed_state() { return this->assumed_state_; } float TemplateSwitch::get_setup_priority() const { return setup_priority::HARDWARE - 2.0f; } -Trigger<> *TemplateSwitch::get_turn_on_trigger() const { return this->turn_on_trigger_; } -Trigger<> *TemplateSwitch::get_turn_off_trigger() const { return this->turn_off_trigger_; } +Trigger<> *TemplateSwitch::get_turn_on_trigger() { return &this->turn_on_trigger_; } +Trigger<> *TemplateSwitch::get_turn_off_trigger() { return &this->turn_off_trigger_; } void TemplateSwitch::setup() { if (!this->f_.has_value()) this->disable_loop(); diff --git a/esphome/components/template/switch/template_switch.h b/esphome/components/template/switch/template_switch.h index 91b7b396f6..1714b4f72b 100644 --- a/esphome/components/template/switch/template_switch.h +++ b/esphome/components/template/switch/template_switch.h @@ -15,8 +15,8 @@ class TemplateSwitch final : public switch_::Switch, public Component { void dump_config() override; template void set_state_lambda(F &&f) { this->f_.set(std::forward(f)); } - Trigger<> *get_turn_on_trigger() const; - Trigger<> *get_turn_off_trigger() const; + Trigger<> *get_turn_on_trigger(); + Trigger<> *get_turn_off_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void loop() override; @@ -31,9 +31,9 @@ class TemplateSwitch final : public switch_::Switch, public Component { TemplateLambda f_; bool optimistic_{false}; bool assumed_state_{false}; - Trigger<> *turn_on_trigger_; - Trigger<> *turn_off_trigger_; - Trigger<> *prev_trigger_{nullptr}; + Trigger<> turn_on_trigger_; + Trigger<> turn_off_trigger_; + Trigger<> *prev_trigger_{nullptr}; // Points to one of the above }; } // namespace esphome::template_ diff --git a/esphome/components/template/text/template_text.cpp b/esphome/components/template/text/template_text.cpp index 70b8dce312..af134e6ed4 100644 --- a/esphome/components/template/text/template_text.cpp +++ b/esphome/components/template/text/template_text.cpp @@ -47,7 +47,7 @@ void TemplateText::update() { } void TemplateText::control(const std::string &value) { - this->set_trigger_->trigger(value); + this->set_trigger_.trigger(value); if (this->optimistic_) this->publish_state(value); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index e5e5e4f4a8..88c6afdf2c 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -68,7 +68,7 @@ class TemplateText final : public text::Text, public PollingComponent { void dump_config() override; float get_setup_priority() const override { return setup_priority::HARDWARE; } - Trigger *get_set_trigger() const { return this->set_trigger_; } + Trigger *get_set_trigger() { return &this->set_trigger_; } void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } void set_initial_value(const char *initial_value) { this->initial_value_ = initial_value; } /// Prevent accidental use of std::string which would dangle @@ -79,7 +79,7 @@ class TemplateText final : public text::Text, public PollingComponent { void control(const std::string &value) override; bool optimistic_ = false; const char *initial_value_{nullptr}; - Trigger *set_trigger_ = new Trigger(); + Trigger set_trigger_; TemplateLambda f_{}; TemplateTextSaverBase *pref_ = nullptr; diff --git a/esphome/components/template/valve/template_valve.cpp b/esphome/components/template/valve/template_valve.cpp index 4e772f9253..2817e1a132 100644 --- a/esphome/components/template/valve/template_valve.cpp +++ b/esphome/components/template/valve/template_valve.cpp @@ -7,12 +7,7 @@ using namespace esphome::valve; static const char *const TAG = "template.valve"; -TemplateValve::TemplateValve() - : open_trigger_(new Trigger<>()), - close_trigger_(new Trigger<>), - stop_trigger_(new Trigger<>()), - toggle_trigger_(new Trigger<>()), - position_trigger_(new Trigger()) {} +TemplateValve::TemplateValve() = default; void TemplateValve::setup() { switch (this->restore_mode_) { @@ -56,10 +51,10 @@ void TemplateValve::set_optimistic(bool optimistic) { this->optimistic_ = optimi void TemplateValve::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } float TemplateValve::get_setup_priority() const { return setup_priority::HARDWARE; } -Trigger<> *TemplateValve::get_open_trigger() const { return this->open_trigger_; } -Trigger<> *TemplateValve::get_close_trigger() const { return this->close_trigger_; } -Trigger<> *TemplateValve::get_stop_trigger() const { return this->stop_trigger_; } -Trigger<> *TemplateValve::get_toggle_trigger() const { return this->toggle_trigger_; } +Trigger<> *TemplateValve::get_open_trigger() { return &this->open_trigger_; } +Trigger<> *TemplateValve::get_close_trigger() { return &this->close_trigger_; } +Trigger<> *TemplateValve::get_stop_trigger() { return &this->stop_trigger_; } +Trigger<> *TemplateValve::get_toggle_trigger() { return &this->toggle_trigger_; } void TemplateValve::dump_config() { LOG_VALVE("", "Template Valve", this); @@ -72,14 +67,14 @@ void TemplateValve::dump_config() { void TemplateValve::control(const ValveCall &call) { if (call.get_stop()) { this->stop_prev_trigger_(); - this->stop_trigger_->trigger(); - this->prev_command_trigger_ = this->stop_trigger_; + this->stop_trigger_.trigger(); + this->prev_command_trigger_ = &this->stop_trigger_; this->publish_state(); } if (call.get_toggle().has_value()) { this->stop_prev_trigger_(); - this->toggle_trigger_->trigger(); - this->prev_command_trigger_ = this->toggle_trigger_; + this->toggle_trigger_.trigger(); + this->prev_command_trigger_ = &this->toggle_trigger_; this->publish_state(); } if (call.get_position().has_value()) { @@ -87,13 +82,13 @@ void TemplateValve::control(const ValveCall &call) { this->stop_prev_trigger_(); if (pos == VALVE_OPEN) { - this->open_trigger_->trigger(); - this->prev_command_trigger_ = this->open_trigger_; + this->open_trigger_.trigger(); + this->prev_command_trigger_ = &this->open_trigger_; } else if (pos == VALVE_CLOSED) { - this->close_trigger_->trigger(); - this->prev_command_trigger_ = this->close_trigger_; + this->close_trigger_.trigger(); + this->prev_command_trigger_ = &this->close_trigger_; } else { - this->position_trigger_->trigger(pos); + this->position_trigger_.trigger(pos); } if (this->optimistic_) { @@ -113,7 +108,7 @@ ValveTraits TemplateValve::get_traits() { return traits; } -Trigger *TemplateValve::get_position_trigger() const { return this->position_trigger_; } +Trigger *TemplateValve::get_position_trigger() { return &this->position_trigger_; } void TemplateValve::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } void TemplateValve::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } diff --git a/esphome/components/template/valve/template_valve.h b/esphome/components/template/valve/template_valve.h index 4205682a2a..76c4630aa0 100644 --- a/esphome/components/template/valve/template_valve.h +++ b/esphome/components/template/valve/template_valve.h @@ -18,11 +18,11 @@ class TemplateValve final : public valve::Valve, public Component { TemplateValve(); template void set_state_lambda(F &&f) { this->state_f_.set(std::forward(f)); } - Trigger<> *get_open_trigger() const; - Trigger<> *get_close_trigger() const; - Trigger<> *get_stop_trigger() const; - Trigger<> *get_toggle_trigger() const; - Trigger *get_position_trigger() const; + Trigger<> *get_open_trigger(); + Trigger<> *get_close_trigger(); + Trigger<> *get_stop_trigger(); + Trigger<> *get_toggle_trigger(); + Trigger *get_position_trigger(); void set_optimistic(bool optimistic); void set_assumed_state(bool assumed_state); void set_has_stop(bool has_stop); @@ -45,14 +45,14 @@ class TemplateValve final : public valve::Valve, public Component { TemplateLambda state_f_; bool assumed_state_{false}; bool optimistic_{false}; - Trigger<> *open_trigger_; - Trigger<> *close_trigger_; + Trigger<> open_trigger_; + Trigger<> close_trigger_; bool has_stop_{false}; bool has_toggle_{false}; - Trigger<> *stop_trigger_; - Trigger<> *toggle_trigger_; + Trigger<> stop_trigger_; + Trigger<> toggle_trigger_; Trigger<> *prev_command_trigger_{nullptr}; - Trigger *position_trigger_; + Trigger position_trigger_; bool has_position_{false}; }; diff --git a/esphome/components/template/water_heater/template_water_heater.cpp b/esphome/components/template/water_heater/template_water_heater.cpp index e89c96ca48..f888edb1df 100644 --- a/esphome/components/template/water_heater/template_water_heater.cpp +++ b/esphome/components/template/water_heater/template_water_heater.cpp @@ -5,7 +5,7 @@ namespace esphome::template_ { static const char *const TAG = "template.water_heater"; -TemplateWaterHeater::TemplateWaterHeater() : set_trigger_(new Trigger<>()) {} +TemplateWaterHeater::TemplateWaterHeater() = default; void TemplateWaterHeater::setup() { if (this->restore_mode_ == TemplateWaterHeaterRestoreMode::WATER_HEATER_RESTORE || @@ -78,7 +78,7 @@ void TemplateWaterHeater::control(const water_heater::WaterHeaterCall &call) { } } - this->set_trigger_->trigger(); + this->set_trigger_.trigger(); if (this->optimistic_) { this->publish_state(); diff --git a/esphome/components/template/water_heater/template_water_heater.h b/esphome/components/template/water_heater/template_water_heater.h index c2a2dcbb23..f1cf00a115 100644 --- a/esphome/components/template/water_heater/template_water_heater.h +++ b/esphome/components/template/water_heater/template_water_heater.h @@ -28,7 +28,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { this->supported_modes_ = modes; } - Trigger<> *get_set_trigger() const { return this->set_trigger_; } + Trigger<> *get_set_trigger() { return &this->set_trigger_; } void setup() override; void loop() override; @@ -42,7 +42,7 @@ class TemplateWaterHeater : public Component, public water_heater::WaterHeater { water_heater::WaterHeaterTraits traits() override; // Ordered to minimize padding on 32-bit: 4-byte members first, then smaller - Trigger<> *set_trigger_; + Trigger<> set_trigger_; TemplateLambda current_temperature_f_; TemplateLambda mode_f_; TemplateWaterHeaterRestoreMode restore_mode_{WATER_HEATER_NO_RESTORE}; diff --git a/esphome/components/thermostat/thermostat_climate.cpp b/esphome/components/thermostat/thermostat_climate.cpp index 44087969b5..c666419701 100644 --- a/esphome/components/thermostat/thermostat_climate.cpp +++ b/esphome/components/thermostat/thermostat_climate.cpp @@ -499,7 +499,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } bool action_ready = false; - Trigger<> *trig = this->idle_action_trigger_, *trig_fan = nullptr; + Trigger<> *trig = &this->idle_action_trigger_, *trig_fan = nullptr; switch (action) { case climate::CLIMATE_ACTION_OFF: case climate::CLIMATE_ACTION_IDLE: @@ -529,10 +529,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); if (this->supports_fan_with_cooling_) { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig_fan = this->fan_only_action_trigger_; + trig_fan = &this->fan_only_action_trigger_; } this->cooling_max_runtime_exceeded_ = false; - trig = this->cool_action_trigger_; + trig = &this->cool_action_trigger_; ESP_LOGVV(TAG, "Switching to COOLING action"); action_ready = true; } @@ -543,10 +543,10 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); if (this->supports_fan_with_heating_) { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig_fan = this->fan_only_action_trigger_; + trig_fan = &this->fan_only_action_trigger_; } this->heating_max_runtime_exceeded_ = false; - trig = this->heat_action_trigger_; + trig = &this->heat_action_trigger_; ESP_LOGVV(TAG, "Switching to HEATING action"); action_ready = true; } @@ -558,7 +558,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } else { this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); } - trig = this->fan_only_action_trigger_; + trig = &this->fan_only_action_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ONLY action"); action_ready = true; } @@ -567,7 +567,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu if (this->drying_action_ready_()) { this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_ON); this->start_timer_(thermostat::THERMOSTAT_TIMER_FANNING_ON); - trig = this->dry_action_trigger_; + trig = &this->dry_action_trigger_; ESP_LOGVV(TAG, "Switching to DRYING action"); action_ready = true; } @@ -586,9 +586,7 @@ void ThermostatClimate::switch_to_action_(climate::ClimateAction action, bool pu } this->action = action; this->prev_action_trigger_ = trig; - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); // if enabled, call the fan_only action with cooling/heating actions if (trig_fan != nullptr) { ESP_LOGVV(TAG, "Calling FAN_ONLY action with HEATING/COOLING action"); @@ -634,14 +632,14 @@ void ThermostatClimate::trigger_supplemental_action_() { if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME)) { this->start_timer_(thermostat::THERMOSTAT_TIMER_COOLING_MAX_RUN_TIME); } - trig = this->supplemental_cool_action_trigger_; + trig = &this->supplemental_cool_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental COOLING action"); break; case climate::CLIMATE_ACTION_HEATING: if (!this->timer_active_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME)) { this->start_timer_(thermostat::THERMOSTAT_TIMER_HEATING_MAX_RUN_TIME); } - trig = this->supplemental_heat_action_trigger_; + trig = &this->supplemental_heat_action_trigger_; ESP_LOGVV(TAG, "Calling supplemental HEATING action"); break; default: @@ -660,24 +658,24 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction return; } - Trigger<> *trig = this->humidity_control_off_action_trigger_; + Trigger<> *trig = &this->humidity_control_off_action_trigger_; switch (action) { case THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF: - // trig = this->humidity_control_off_action_trigger_; + // trig = &this->humidity_control_off_action_trigger_; ESP_LOGVV(TAG, "Switching to HUMIDIFICATION_OFF action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_DEHUMIDIFY: - trig = this->humidity_control_dehumidify_action_trigger_; + trig = &this->humidity_control_dehumidify_action_trigger_; ESP_LOGVV(TAG, "Switching to DEHUMIDIFY action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_HUMIDIFY: - trig = this->humidity_control_humidify_action_trigger_; + trig = &this->humidity_control_humidify_action_trigger_; ESP_LOGVV(TAG, "Switching to HUMIDIFY action"); break; case THERMOSTAT_HUMIDITY_CONTROL_ACTION_NONE: default: action = THERMOSTAT_HUMIDITY_CONTROL_ACTION_OFF; - // trig = this->humidity_control_off_action_trigger_; + // trig = &this->humidity_control_off_action_trigger_; } if (this->prev_humidity_control_trigger_ != nullptr) { @@ -686,9 +684,7 @@ void ThermostatClimate::switch_to_humidity_control_action_(HumidificationAction } this->humidification_action = action; this->prev_humidity_control_trigger_ = trig; - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); } void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bool publish_state) { @@ -703,62 +699,60 @@ void ThermostatClimate::switch_to_fan_mode_(climate::ClimateFanMode fan_mode, bo this->publish_state(); if (this->fan_mode_ready_()) { - Trigger<> *trig = this->fan_mode_auto_trigger_; + Trigger<> *trig = &this->fan_mode_auto_trigger_; switch (fan_mode) { case climate::CLIMATE_FAN_ON: - trig = this->fan_mode_on_trigger_; + trig = &this->fan_mode_on_trigger_; ESP_LOGVV(TAG, "Switching to FAN_ON mode"); break; case climate::CLIMATE_FAN_OFF: - trig = this->fan_mode_off_trigger_; + trig = &this->fan_mode_off_trigger_; ESP_LOGVV(TAG, "Switching to FAN_OFF mode"); break; case climate::CLIMATE_FAN_AUTO: - // trig = this->fan_mode_auto_trigger_; + // trig = &this->fan_mode_auto_trigger_; ESP_LOGVV(TAG, "Switching to FAN_AUTO mode"); break; case climate::CLIMATE_FAN_LOW: - trig = this->fan_mode_low_trigger_; + trig = &this->fan_mode_low_trigger_; ESP_LOGVV(TAG, "Switching to FAN_LOW mode"); break; case climate::CLIMATE_FAN_MEDIUM: - trig = this->fan_mode_medium_trigger_; + trig = &this->fan_mode_medium_trigger_; ESP_LOGVV(TAG, "Switching to FAN_MEDIUM mode"); break; case climate::CLIMATE_FAN_HIGH: - trig = this->fan_mode_high_trigger_; + trig = &this->fan_mode_high_trigger_; ESP_LOGVV(TAG, "Switching to FAN_HIGH mode"); break; case climate::CLIMATE_FAN_MIDDLE: - trig = this->fan_mode_middle_trigger_; + trig = &this->fan_mode_middle_trigger_; ESP_LOGVV(TAG, "Switching to FAN_MIDDLE mode"); break; case climate::CLIMATE_FAN_FOCUS: - trig = this->fan_mode_focus_trigger_; + trig = &this->fan_mode_focus_trigger_; ESP_LOGVV(TAG, "Switching to FAN_FOCUS mode"); break; case climate::CLIMATE_FAN_DIFFUSE: - trig = this->fan_mode_diffuse_trigger_; + trig = &this->fan_mode_diffuse_trigger_; ESP_LOGVV(TAG, "Switching to FAN_DIFFUSE mode"); break; case climate::CLIMATE_FAN_QUIET: - trig = this->fan_mode_quiet_trigger_; + trig = &this->fan_mode_quiet_trigger_; ESP_LOGVV(TAG, "Switching to FAN_QUIET mode"); break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value fan_mode = climate::CLIMATE_FAN_AUTO; - // trig = this->fan_mode_auto_trigger_; + // trig = &this->fan_mode_auto_trigger_; } if (this->prev_fan_mode_trigger_ != nullptr) { this->prev_fan_mode_trigger_->stop_action(); this->prev_fan_mode_trigger_ = nullptr; } this->start_timer_(thermostat::THERMOSTAT_TIMER_FAN_MODE); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->prev_fan_mode_ = fan_mode; this->prev_fan_mode_trigger_ = trig; } @@ -775,25 +769,25 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ this->prev_mode_trigger_->stop_action(); this->prev_mode_trigger_ = nullptr; } - Trigger<> *trig = this->off_mode_trigger_; + Trigger<> *trig = &this->off_mode_trigger_; switch (mode) { case climate::CLIMATE_MODE_AUTO: - trig = this->auto_mode_trigger_; + trig = &this->auto_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT_COOL: - trig = this->heat_cool_mode_trigger_; + trig = &this->heat_cool_mode_trigger_; break; case climate::CLIMATE_MODE_COOL: - trig = this->cool_mode_trigger_; + trig = &this->cool_mode_trigger_; break; case climate::CLIMATE_MODE_HEAT: - trig = this->heat_mode_trigger_; + trig = &this->heat_mode_trigger_; break; case climate::CLIMATE_MODE_FAN_ONLY: - trig = this->fan_only_mode_trigger_; + trig = &this->fan_only_mode_trigger_; break; case climate::CLIMATE_MODE_DRY: - trig = this->dry_mode_trigger_; + trig = &this->dry_mode_trigger_; break; case climate::CLIMATE_MODE_OFF: default: @@ -802,9 +796,7 @@ void ThermostatClimate::switch_to_mode_(climate::ClimateMode mode, bool publish_ mode = climate::CLIMATE_MODE_OFF; // trig = this->off_mode_trigger_; } - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->mode = mode; this->prev_mode_ = mode; this->prev_mode_trigger_ = trig; @@ -824,29 +816,27 @@ void ThermostatClimate::switch_to_swing_mode_(climate::ClimateSwingMode swing_mo this->prev_swing_mode_trigger_->stop_action(); this->prev_swing_mode_trigger_ = nullptr; } - Trigger<> *trig = this->swing_mode_off_trigger_; + Trigger<> *trig = &this->swing_mode_off_trigger_; switch (swing_mode) { case climate::CLIMATE_SWING_BOTH: - trig = this->swing_mode_both_trigger_; + trig = &this->swing_mode_both_trigger_; break; case climate::CLIMATE_SWING_HORIZONTAL: - trig = this->swing_mode_horizontal_trigger_; + trig = &this->swing_mode_horizontal_trigger_; break; case climate::CLIMATE_SWING_OFF: - // trig = this->swing_mode_off_trigger_; + // trig = &this->swing_mode_off_trigger_; break; case climate::CLIMATE_SWING_VERTICAL: - trig = this->swing_mode_vertical_trigger_; + trig = &this->swing_mode_vertical_trigger_; break; default: // we cannot report an invalid mode back to HA (even if it asked for one) // and must assume some valid value swing_mode = climate::CLIMATE_SWING_OFF; - // trig = this->swing_mode_off_trigger_; - } - if (trig != nullptr) { - trig->trigger(); + // trig = &this->swing_mode_off_trigger_; } + trig->trigger(); this->swing_mode = swing_mode; this->prev_swing_mode_ = swing_mode; this->prev_swing_mode_trigger_ = trig; @@ -1024,10 +1014,8 @@ void ThermostatClimate::check_humidity_change_trigger_() { this->prev_target_humidity_ = this->target_humidity; } // trigger the action - Trigger<> *trig = this->humidity_change_trigger_; - if (trig != nullptr) { - trig->trigger(); - } + Trigger<> *trig = &this->humidity_change_trigger_; + trig->trigger(); } void ThermostatClimate::check_temperature_change_trigger_() { @@ -1050,10 +1038,8 @@ void ThermostatClimate::check_temperature_change_trigger_() { } } // trigger the action - Trigger<> *trig = this->temperature_change_trigger_; - if (trig != nullptr) { - trig->trigger(); - } + Trigger<> *trig = &this->temperature_change_trigger_; + trig->trigger(); } bool ThermostatClimate::cooling_required_() { @@ -1202,12 +1188,10 @@ void ThermostatClimate::change_preset_(climate::ClimatePreset preset) { if (config != nullptr) { ESP_LOGV(TAG, "Preset %s requested", LOG_STR_ARG(climate::climate_preset_to_string(preset))); if (this->change_preset_internal_(*config) || (!this->preset.has_value()) || this->preset.value() != preset) { - // Fire any preset changed trigger if defined - Trigger<> *trig = this->preset_change_trigger_; + // Fire preset changed trigger + Trigger<> *trig = &this->preset_change_trigger_; this->set_preset_(preset); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->refresh(); ESP_LOGI(TAG, "Preset %s applied", LOG_STR_ARG(climate::climate_preset_to_string(preset))); @@ -1234,13 +1218,11 @@ void ThermostatClimate::change_custom_preset_(const char *custom_preset, size_t ESP_LOGV(TAG, "Custom preset %s requested", custom_preset); if (this->change_preset_internal_(*config) || !this->has_custom_preset() || this->get_custom_preset() != custom_preset) { - // Fire any preset changed trigger if defined - Trigger<> *trig = this->preset_change_trigger_; + // Fire preset changed trigger + Trigger<> *trig = &this->preset_change_trigger_; // Use the base class method which handles pointer lookup and preset reset internally this->set_custom_preset_(custom_preset); - if (trig != nullptr) { - trig->trigger(); - } + trig->trigger(); this->refresh(); ESP_LOGI(TAG, "Custom preset %s applied", custom_preset); @@ -1305,41 +1287,7 @@ void ThermostatClimate::set_custom_preset_config(std::initializer_listcustom_preset_config_ = presets; } -ThermostatClimate::ThermostatClimate() - : cool_action_trigger_(new Trigger<>()), - supplemental_cool_action_trigger_(new Trigger<>()), - cool_mode_trigger_(new Trigger<>()), - dry_action_trigger_(new Trigger<>()), - dry_mode_trigger_(new Trigger<>()), - heat_action_trigger_(new Trigger<>()), - supplemental_heat_action_trigger_(new Trigger<>()), - heat_mode_trigger_(new Trigger<>()), - heat_cool_mode_trigger_(new Trigger<>()), - auto_mode_trigger_(new Trigger<>()), - idle_action_trigger_(new Trigger<>()), - off_mode_trigger_(new Trigger<>()), - fan_only_action_trigger_(new Trigger<>()), - fan_only_mode_trigger_(new Trigger<>()), - fan_mode_on_trigger_(new Trigger<>()), - fan_mode_off_trigger_(new Trigger<>()), - fan_mode_auto_trigger_(new Trigger<>()), - fan_mode_low_trigger_(new Trigger<>()), - fan_mode_medium_trigger_(new Trigger<>()), - fan_mode_high_trigger_(new Trigger<>()), - fan_mode_middle_trigger_(new Trigger<>()), - fan_mode_focus_trigger_(new Trigger<>()), - fan_mode_diffuse_trigger_(new Trigger<>()), - fan_mode_quiet_trigger_(new Trigger<>()), - swing_mode_both_trigger_(new Trigger<>()), - swing_mode_off_trigger_(new Trigger<>()), - swing_mode_horizontal_trigger_(new Trigger<>()), - swing_mode_vertical_trigger_(new Trigger<>()), - humidity_change_trigger_(new Trigger<>()), - temperature_change_trigger_(new Trigger<>()), - preset_change_trigger_(new Trigger<>()), - humidity_control_dehumidify_action_trigger_(new Trigger<>()), - humidity_control_humidify_action_trigger_(new Trigger<>()), - humidity_control_off_action_trigger_(new Trigger<>()) {} +ThermostatClimate::ThermostatClimate() = default; void ThermostatClimate::set_default_preset(const char *custom_preset) { // Find the preset in custom_preset_config_ and store pointer from there @@ -1513,49 +1461,49 @@ void ThermostatClimate::set_supports_humidification(bool supports_humidification } } -Trigger<> *ThermostatClimate::get_cool_action_trigger() const { return this->cool_action_trigger_; } -Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() const { - return this->supplemental_cool_action_trigger_; +Trigger<> *ThermostatClimate::get_cool_action_trigger() { return &this->cool_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_cool_action_trigger() { + return &this->supplemental_cool_action_trigger_; } -Trigger<> *ThermostatClimate::get_dry_action_trigger() const { return this->dry_action_trigger_; } -Trigger<> *ThermostatClimate::get_fan_only_action_trigger() const { return this->fan_only_action_trigger_; } -Trigger<> *ThermostatClimate::get_heat_action_trigger() const { return this->heat_action_trigger_; } -Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() const { - return this->supplemental_heat_action_trigger_; +Trigger<> *ThermostatClimate::get_dry_action_trigger() { return &this->dry_action_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_action_trigger() { return &this->fan_only_action_trigger_; } +Trigger<> *ThermostatClimate::get_heat_action_trigger() { return &this->heat_action_trigger_; } +Trigger<> *ThermostatClimate::get_supplemental_heat_action_trigger() { + return &this->supplemental_heat_action_trigger_; } -Trigger<> *ThermostatClimate::get_idle_action_trigger() const { return this->idle_action_trigger_; } -Trigger<> *ThermostatClimate::get_auto_mode_trigger() const { return this->auto_mode_trigger_; } -Trigger<> *ThermostatClimate::get_cool_mode_trigger() const { return this->cool_mode_trigger_; } -Trigger<> *ThermostatClimate::get_dry_mode_trigger() const { return this->dry_mode_trigger_; } -Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() const { return this->fan_only_mode_trigger_; } -Trigger<> *ThermostatClimate::get_heat_mode_trigger() const { return this->heat_mode_trigger_; } -Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() const { return this->heat_cool_mode_trigger_; } -Trigger<> *ThermostatClimate::get_off_mode_trigger() const { return this->off_mode_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() const { return this->fan_mode_on_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() const { return this->fan_mode_off_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() const { return this->fan_mode_auto_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() const { return this->fan_mode_low_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() const { return this->fan_mode_medium_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() const { return this->fan_mode_high_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() const { return this->fan_mode_middle_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() const { return this->fan_mode_focus_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() const { return this->fan_mode_diffuse_trigger_; } -Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() const { return this->fan_mode_quiet_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() const { return this->swing_mode_both_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() const { return this->swing_mode_off_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() const { return this->swing_mode_horizontal_trigger_; } -Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() const { return this->swing_mode_vertical_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_change_trigger() const { return this->humidity_change_trigger_; } -Trigger<> *ThermostatClimate::get_temperature_change_trigger() const { return this->temperature_change_trigger_; } -Trigger<> *ThermostatClimate::get_preset_change_trigger() const { return this->preset_change_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() const { - return this->humidity_control_dehumidify_action_trigger_; +Trigger<> *ThermostatClimate::get_idle_action_trigger() { return &this->idle_action_trigger_; } +Trigger<> *ThermostatClimate::get_auto_mode_trigger() { return &this->auto_mode_trigger_; } +Trigger<> *ThermostatClimate::get_cool_mode_trigger() { return &this->cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_dry_mode_trigger() { return &this->dry_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_only_mode_trigger() { return &this->fan_only_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_mode_trigger() { return &this->heat_mode_trigger_; } +Trigger<> *ThermostatClimate::get_heat_cool_mode_trigger() { return &this->heat_cool_mode_trigger_; } +Trigger<> *ThermostatClimate::get_off_mode_trigger() { return &this->off_mode_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_on_trigger() { return &this->fan_mode_on_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_off_trigger() { return &this->fan_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_auto_trigger() { return &this->fan_mode_auto_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_low_trigger() { return &this->fan_mode_low_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_medium_trigger() { return &this->fan_mode_medium_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_high_trigger() { return &this->fan_mode_high_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_middle_trigger() { return &this->fan_mode_middle_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_focus_trigger() { return &this->fan_mode_focus_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_diffuse_trigger() { return &this->fan_mode_diffuse_trigger_; } +Trigger<> *ThermostatClimate::get_fan_mode_quiet_trigger() { return &this->fan_mode_quiet_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_both_trigger() { return &this->swing_mode_both_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_off_trigger() { return &this->swing_mode_off_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_horizontal_trigger() { return &this->swing_mode_horizontal_trigger_; } +Trigger<> *ThermostatClimate::get_swing_mode_vertical_trigger() { return &this->swing_mode_vertical_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_change_trigger() { return &this->humidity_change_trigger_; } +Trigger<> *ThermostatClimate::get_temperature_change_trigger() { return &this->temperature_change_trigger_; } +Trigger<> *ThermostatClimate::get_preset_change_trigger() { return &this->preset_change_trigger_; } +Trigger<> *ThermostatClimate::get_humidity_control_dehumidify_action_trigger() { + return &this->humidity_control_dehumidify_action_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() const { - return this->humidity_control_humidify_action_trigger_; +Trigger<> *ThermostatClimate::get_humidity_control_humidify_action_trigger() { + return &this->humidity_control_humidify_action_trigger_; } -Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() const { - return this->humidity_control_off_action_trigger_; +Trigger<> *ThermostatClimate::get_humidity_control_off_action_trigger() { + return &this->humidity_control_off_action_trigger_; } void ThermostatClimate::dump_config() { diff --git a/esphome/components/thermostat/thermostat_climate.h b/esphome/components/thermostat/thermostat_climate.h index d37c9a68a6..4268d5c582 100644 --- a/esphome/components/thermostat/thermostat_climate.h +++ b/esphome/components/thermostat/thermostat_climate.h @@ -146,40 +146,40 @@ class ThermostatClimate : public climate::Climate, public Component { void set_preset_config(std::initializer_list presets); void set_custom_preset_config(std::initializer_list presets); - Trigger<> *get_cool_action_trigger() const; - Trigger<> *get_supplemental_cool_action_trigger() const; - Trigger<> *get_dry_action_trigger() const; - Trigger<> *get_fan_only_action_trigger() const; - Trigger<> *get_heat_action_trigger() const; - Trigger<> *get_supplemental_heat_action_trigger() const; - Trigger<> *get_idle_action_trigger() const; - Trigger<> *get_auto_mode_trigger() const; - Trigger<> *get_cool_mode_trigger() const; - Trigger<> *get_dry_mode_trigger() const; - Trigger<> *get_fan_only_mode_trigger() const; - Trigger<> *get_heat_mode_trigger() const; - Trigger<> *get_heat_cool_mode_trigger() const; - Trigger<> *get_off_mode_trigger() const; - Trigger<> *get_fan_mode_on_trigger() const; - Trigger<> *get_fan_mode_off_trigger() const; - Trigger<> *get_fan_mode_auto_trigger() const; - Trigger<> *get_fan_mode_low_trigger() const; - Trigger<> *get_fan_mode_medium_trigger() const; - Trigger<> *get_fan_mode_high_trigger() const; - Trigger<> *get_fan_mode_middle_trigger() const; - Trigger<> *get_fan_mode_focus_trigger() const; - Trigger<> *get_fan_mode_diffuse_trigger() const; - Trigger<> *get_fan_mode_quiet_trigger() const; - Trigger<> *get_swing_mode_both_trigger() const; - Trigger<> *get_swing_mode_horizontal_trigger() const; - Trigger<> *get_swing_mode_off_trigger() const; - Trigger<> *get_swing_mode_vertical_trigger() const; - Trigger<> *get_humidity_change_trigger() const; - Trigger<> *get_temperature_change_trigger() const; - Trigger<> *get_preset_change_trigger() const; - Trigger<> *get_humidity_control_dehumidify_action_trigger() const; - Trigger<> *get_humidity_control_humidify_action_trigger() const; - Trigger<> *get_humidity_control_off_action_trigger() const; + Trigger<> *get_cool_action_trigger(); + Trigger<> *get_supplemental_cool_action_trigger(); + Trigger<> *get_dry_action_trigger(); + Trigger<> *get_fan_only_action_trigger(); + Trigger<> *get_heat_action_trigger(); + Trigger<> *get_supplemental_heat_action_trigger(); + Trigger<> *get_idle_action_trigger(); + Trigger<> *get_auto_mode_trigger(); + Trigger<> *get_cool_mode_trigger(); + Trigger<> *get_dry_mode_trigger(); + Trigger<> *get_fan_only_mode_trigger(); + Trigger<> *get_heat_mode_trigger(); + Trigger<> *get_heat_cool_mode_trigger(); + Trigger<> *get_off_mode_trigger(); + Trigger<> *get_fan_mode_on_trigger(); + Trigger<> *get_fan_mode_off_trigger(); + Trigger<> *get_fan_mode_auto_trigger(); + Trigger<> *get_fan_mode_low_trigger(); + Trigger<> *get_fan_mode_medium_trigger(); + Trigger<> *get_fan_mode_high_trigger(); + Trigger<> *get_fan_mode_middle_trigger(); + Trigger<> *get_fan_mode_focus_trigger(); + Trigger<> *get_fan_mode_diffuse_trigger(); + Trigger<> *get_fan_mode_quiet_trigger(); + Trigger<> *get_swing_mode_both_trigger(); + Trigger<> *get_swing_mode_horizontal_trigger(); + Trigger<> *get_swing_mode_off_trigger(); + Trigger<> *get_swing_mode_vertical_trigger(); + Trigger<> *get_humidity_change_trigger(); + Trigger<> *get_temperature_change_trigger(); + Trigger<> *get_preset_change_trigger(); + Trigger<> *get_humidity_control_dehumidify_action_trigger(); + Trigger<> *get_humidity_control_humidify_action_trigger(); + Trigger<> *get_humidity_control_off_action_trigger(); /// Get current hysteresis values float cool_deadband(); float cool_overrun(); @@ -417,115 +417,65 @@ class ThermostatClimate : public climate::Climate, public Component { /// The sensor used for getting the current humidity sensor::Sensor *humidity_sensor_{nullptr}; - /// The trigger to call when the controller should switch to cooling action/mode. - /// - /// A null value for this attribute means that the controller has no cooling action - /// For example electric heat, where only heating (power on) and not-heating - /// (power off) is possible. - Trigger<> *cool_action_trigger_{nullptr}; - Trigger<> *supplemental_cool_action_trigger_{nullptr}; - Trigger<> *cool_mode_trigger_{nullptr}; + /// Trigger for cooling action/mode + Trigger<> cool_action_trigger_; + Trigger<> supplemental_cool_action_trigger_; + Trigger<> cool_mode_trigger_; - /// The trigger to call when the controller should switch to dry (dehumidification) mode. - /// - /// In dry mode, the controller is assumed to have both heating and cooling disabled, - /// although the system may use its cooling mechanism to achieve drying. - Trigger<> *dry_action_trigger_{nullptr}; - Trigger<> *dry_mode_trigger_{nullptr}; + /// Trigger for dry (dehumidification) mode + Trigger<> dry_action_trigger_; + Trigger<> dry_mode_trigger_; - /// The trigger to call when the controller should switch to heating action/mode. - /// - /// A null value for this attribute means that the controller has no heating action - /// For example window blinds, where only cooling (blinds closed) and not-cooling - /// (blinds open) is possible. - Trigger<> *heat_action_trigger_{nullptr}; - Trigger<> *supplemental_heat_action_trigger_{nullptr}; - Trigger<> *heat_mode_trigger_{nullptr}; + /// Trigger for heating action/mode + Trigger<> heat_action_trigger_; + Trigger<> supplemental_heat_action_trigger_; + Trigger<> heat_mode_trigger_; - /// The trigger to call when the controller should switch to heat/cool mode. - /// - /// In heat/cool mode, the controller will enable heating/cooling as necessary and switch - /// to idle when the temperature is within the thresholds/set points. - Trigger<> *heat_cool_mode_trigger_{nullptr}; + /// Trigger for heat/cool mode + Trigger<> heat_cool_mode_trigger_; - /// The trigger to call when the controller should switch to auto mode. - /// - /// In auto mode, the controller will enable heating/cooling as supported/necessary and switch - /// to idle when the temperature is within the thresholds/set points. - Trigger<> *auto_mode_trigger_{nullptr}; + /// Trigger for auto mode + Trigger<> auto_mode_trigger_; - /// The trigger to call when the controller should switch to idle action/off mode. - /// - /// In these actions/modes, the controller is assumed to have both heating and cooling disabled. - Trigger<> *idle_action_trigger_{nullptr}; - Trigger<> *off_mode_trigger_{nullptr}; + /// Trigger for idle action/off mode + Trigger<> idle_action_trigger_; + Trigger<> off_mode_trigger_; - /// The trigger to call when the controller should switch to fan-only action/mode. - /// - /// In fan-only mode, the controller is assumed to have both heating and cooling disabled. - /// The system should activate the fan only. - Trigger<> *fan_only_action_trigger_{nullptr}; - Trigger<> *fan_only_mode_trigger_{nullptr}; + /// Trigger for fan-only action/mode + Trigger<> fan_only_action_trigger_; + Trigger<> fan_only_mode_trigger_; - /// The trigger to call when the controller should switch on the fan. - Trigger<> *fan_mode_on_trigger_{nullptr}; + /// Fan mode triggers + Trigger<> fan_mode_on_trigger_; + Trigger<> fan_mode_off_trigger_; + Trigger<> fan_mode_auto_trigger_; + Trigger<> fan_mode_low_trigger_; + Trigger<> fan_mode_medium_trigger_; + Trigger<> fan_mode_high_trigger_; + Trigger<> fan_mode_middle_trigger_; + Trigger<> fan_mode_focus_trigger_; + Trigger<> fan_mode_diffuse_trigger_; + Trigger<> fan_mode_quiet_trigger_; - /// The trigger to call when the controller should switch off the fan. - Trigger<> *fan_mode_off_trigger_{nullptr}; + /// Swing mode triggers + Trigger<> swing_mode_both_trigger_; + Trigger<> swing_mode_off_trigger_; + Trigger<> swing_mode_horizontal_trigger_; + Trigger<> swing_mode_vertical_trigger_; - /// The trigger to call when the controller should switch the fan to "auto" mode. - Trigger<> *fan_mode_auto_trigger_{nullptr}; + /// Trigger for target humidity changes + Trigger<> humidity_change_trigger_; - /// The trigger to call when the controller should switch the fan to "low" speed. - Trigger<> *fan_mode_low_trigger_{nullptr}; + /// Trigger for target temperature changes + Trigger<> temperature_change_trigger_; - /// The trigger to call when the controller should switch the fan to "medium" speed. - Trigger<> *fan_mode_medium_trigger_{nullptr}; + /// Trigger for preset mode changes + Trigger<> preset_change_trigger_; - /// The trigger to call when the controller should switch the fan to "high" speed. - Trigger<> *fan_mode_high_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "middle" position. - Trigger<> *fan_mode_middle_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "focus" position. - Trigger<> *fan_mode_focus_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "diffuse" position. - Trigger<> *fan_mode_diffuse_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the fan to "quiet" position. - Trigger<> *fan_mode_quiet_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "both". - Trigger<> *swing_mode_both_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "off". - Trigger<> *swing_mode_off_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "horizontal". - Trigger<> *swing_mode_horizontal_trigger_{nullptr}; - - /// The trigger to call when the controller should switch the swing mode to "vertical". - Trigger<> *swing_mode_vertical_trigger_{nullptr}; - - /// The trigger to call when the target humidity changes. - Trigger<> *humidity_change_trigger_{nullptr}; - - /// The trigger to call when the target temperature(s) change(es). - Trigger<> *temperature_change_trigger_{nullptr}; - - /// The trigger to call when the preset mode changes - Trigger<> *preset_change_trigger_{nullptr}; - - /// The trigger to call when dehumidification is required - Trigger<> *humidity_control_dehumidify_action_trigger_{nullptr}; - - /// The trigger to call when humidification is required - Trigger<> *humidity_control_humidify_action_trigger_{nullptr}; - - /// The trigger to call when (de)humidification should stop - Trigger<> *humidity_control_off_action_trigger_{nullptr}; + /// Humidity control triggers + Trigger<> humidity_control_dehumidify_action_trigger_; + Trigger<> humidity_control_humidify_action_trigger_; + Trigger<> humidity_control_off_action_trigger_; /// A reference to the trigger that was previously active. /// diff --git a/esphome/components/time/real_time_clock.h b/esphome/components/time/real_time_clock.h index 70469e11b0..19aa1a4f4a 100644 --- a/esphome/components/time/real_time_clock.h +++ b/esphome/components/time/real_time_clock.h @@ -62,7 +62,7 @@ class RealTimeClock : public PollingComponent { void apply_timezone_(); #endif - CallbackManager time_sync_callback_; + LazyCallbackManager time_sync_callback_; }; template class TimeHasTimeCondition : public Condition { diff --git a/esphome/components/time_based/time_based_cover.cpp b/esphome/components/time_based/time_based_cover.cpp index 1eb591fe6e..f6a3048bd4 100644 --- a/esphome/components/time_based/time_based_cover.cpp +++ b/esphome/components/time_based/time_based_cover.cpp @@ -51,7 +51,7 @@ void TimeBasedCover::loop() { this->last_publish_time_ = now; } } -float TimeBasedCover::get_setup_priority() const { return setup_priority::DATA; } + CoverTraits TimeBasedCover::get_traits() { auto traits = CoverTraits(); traits.set_supports_stop(true); @@ -132,15 +132,15 @@ void TimeBasedCover::start_direction_(CoverOperation dir) { Trigger<> *trig; switch (dir) { case COVER_OPERATION_IDLE: - trig = this->stop_trigger_; + trig = &this->stop_trigger_; break; case COVER_OPERATION_OPENING: this->last_operation_ = dir; - trig = this->open_trigger_; + trig = &this->open_trigger_; break; case COVER_OPERATION_CLOSING: this->last_operation_ = dir; - trig = this->close_trigger_; + trig = &this->close_trigger_; break; default: return; diff --git a/esphome/components/time_based/time_based_cover.h b/esphome/components/time_based/time_based_cover.h index 42cf66c2ab..0adc5cb370 100644 --- a/esphome/components/time_based/time_based_cover.h +++ b/esphome/components/time_based/time_based_cover.h @@ -12,11 +12,10 @@ class TimeBasedCover : public cover::Cover, public Component { void setup() override; void loop() override; void dump_config() override; - float get_setup_priority() const override; - Trigger<> *get_open_trigger() const { return this->open_trigger_; } - Trigger<> *get_close_trigger() const { return this->close_trigger_; } - Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } + Trigger<> *get_open_trigger() { return &this->open_trigger_; } + Trigger<> *get_close_trigger() { return &this->close_trigger_; } + Trigger<> *get_stop_trigger() { return &this->stop_trigger_; } void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; } void set_close_duration(uint32_t close_duration) { this->close_duration_ = close_duration; } cover::CoverTraits get_traits() override; @@ -34,11 +33,11 @@ class TimeBasedCover : public cover::Cover, public Component { void recompute_position_(); - Trigger<> *open_trigger_{new Trigger<>()}; + Trigger<> open_trigger_; uint32_t open_duration_; - Trigger<> *close_trigger_{new Trigger<>()}; + Trigger<> close_trigger_; uint32_t close_duration_; - Trigger<> *stop_trigger_{new Trigger<>()}; + Trigger<> stop_trigger_; Trigger<> *prev_command_trigger_{nullptr}; uint32_t last_recompute_time_{0}; diff --git a/esphome/components/tmp102/tmp102.cpp b/esphome/components/tmp102/tmp102.cpp index 7390d9fcc9..99f6753ddc 100644 --- a/esphome/components/tmp102/tmp102.cpp +++ b/esphome/components/tmp102/tmp102.cpp @@ -46,7 +46,5 @@ void TMP102Component::update() { }); } -float TMP102Component::get_setup_priority() const { return setup_priority::DATA; } - } // namespace tmp102 } // namespace esphome diff --git a/esphome/components/tmp102/tmp102.h b/esphome/components/tmp102/tmp102.h index 657b48c7cf..fe860a3819 100644 --- a/esphome/components/tmp102/tmp102.h +++ b/esphome/components/tmp102/tmp102.h @@ -11,8 +11,6 @@ class TMP102Component : public PollingComponent, public i2c::I2CDevice, public s public: void dump_config() override; void update() override; - - float get_setup_priority() const override; }; } // namespace tmp102 diff --git a/esphome/components/tmp117/tmp117.cpp b/esphome/components/tmp117/tmp117.cpp index c9eff41399..f8f52266e0 100644 --- a/esphome/components/tmp117/tmp117.cpp +++ b/esphome/components/tmp117/tmp117.cpp @@ -45,7 +45,7 @@ void TMP117Component::dump_config() { } LOG_SENSOR(" ", "Temperature", this); } -float TMP117Component::get_setup_priority() const { return setup_priority::DATA; } + bool TMP117Component::read_data_(int16_t *data) { if (!this->read_byte_16(0, (uint16_t *) data)) { ESP_LOGW(TAG, "Updating TMP117 failed!"); diff --git a/esphome/components/tmp117/tmp117.h b/esphome/components/tmp117/tmp117.h index 162dbb64db..f501ee270c 100644 --- a/esphome/components/tmp117/tmp117.h +++ b/esphome/components/tmp117/tmp117.h @@ -11,7 +11,6 @@ class TMP117Component : public PollingComponent, public i2c::I2CDevice, public s public: void setup() override; void dump_config() override; - float get_setup_priority() const override; void update() override; void set_config(uint16_t config) { config_ = config; }; diff --git a/esphome/components/tsl2561/tsl2561.cpp b/esphome/components/tsl2561/tsl2561.cpp index 1442dd176c..cb4c38a83c 100644 --- a/esphome/components/tsl2561/tsl2561.cpp +++ b/esphome/components/tsl2561/tsl2561.cpp @@ -144,7 +144,7 @@ void TSL2561Sensor::set_integration_time(TSL2561IntegrationTime integration_time } void TSL2561Sensor::set_gain(TSL2561Gain gain) { this->gain_ = gain; } void TSL2561Sensor::set_is_cs_package(bool package_cs) { this->package_cs_ = package_cs; } -float TSL2561Sensor::get_setup_priority() const { return setup_priority::DATA; } + bool TSL2561Sensor::tsl2561_write_byte(uint8_t a_register, uint8_t value) { return this->write_byte(a_register | TSL2561_COMMAND_BIT, value); } diff --git a/esphome/components/tsl2561/tsl2561.h b/esphome/components/tsl2561/tsl2561.h index c54f41fb81..a8f0aef90f 100644 --- a/esphome/components/tsl2561/tsl2561.h +++ b/esphome/components/tsl2561/tsl2561.h @@ -67,7 +67,6 @@ class TSL2561Sensor : public sensor::Sensor, public PollingComponent, public i2c void setup() override; void dump_config() override; void update() override; - float get_setup_priority() const override; bool tsl2561_read_byte(uint8_t a_register, uint8_t *value); bool tsl2561_read_uint(uint8_t a_register, uint16_t *value); diff --git a/esphome/components/tsl2591/tsl2591.cpp b/esphome/components/tsl2591/tsl2591.cpp index 999e42e949..42c524a074 100644 --- a/esphome/components/tsl2591/tsl2591.cpp +++ b/esphome/components/tsl2591/tsl2591.cpp @@ -247,8 +247,6 @@ void TSL2591Component::set_power_save_mode(bool enable) { this->power_save_mode_ void TSL2591Component::set_name(const char *name) { this->name_ = name; } -float TSL2591Component::get_setup_priority() const { return setup_priority::DATA; } - bool TSL2591Component::is_adc_valid() { uint8_t status; if (!this->read_byte(TSL2591_COMMAND_BIT | TSL2591_REGISTER_STATUS, &status)) { diff --git a/esphome/components/tsl2591/tsl2591.h b/esphome/components/tsl2591/tsl2591.h index fa302b14b0..84c92b6ba9 100644 --- a/esphome/components/tsl2591/tsl2591.h +++ b/esphome/components/tsl2591/tsl2591.h @@ -249,8 +249,6 @@ class TSL2591Component : public PollingComponent, public i2c::I2CDevice { void dump_config() override; /** Used by ESPHome framework. */ void update() override; - /** Used by ESPHome framework. */ - float get_setup_priority() const override; protected: const char *name_; diff --git a/esphome/components/tx20/tx20.cpp b/esphome/components/tx20/tx20.cpp index fd7b5fb03f..6bc5f0bb51 100644 --- a/esphome/components/tx20/tx20.cpp +++ b/esphome/components/tx20/tx20.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -#include +#include namespace esphome { namespace tx20 { @@ -38,32 +38,30 @@ void Tx20Component::loop() { } } -float Tx20Component::get_setup_priority() const { return setup_priority::DATA; } - std::string Tx20Component::get_wind_cardinal_direction() const { return this->wind_cardinal_direction_; } void Tx20Component::decode_and_publish_() { ESP_LOGVV(TAG, "Decode Tx20"); - std::string string_buffer; - std::string string_buffer_2; - std::vector bit_buffer; + std::array bit_buffer{}; + size_t bit_pos = 0; bool current_bit = true; + // Cap at MAX_BUFFER_SIZE - 1 to prevent out-of-bounds access (buffer_index can exceed MAX_BUFFER_SIZE in ISR) + const int max_buffer_index = + std::min(static_cast(this->store_.buffer_index), static_cast(MAX_BUFFER_SIZE - 1)); - for (int i = 1; i <= this->store_.buffer_index; i++) { - string_buffer_2 += to_string(this->store_.buffer[i]) + ", "; + for (int i = 1; i <= max_buffer_index; i++) { uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME; // ignore segments at the end that were too short - string_buffer.append(repeat, current_bit ? '1' : '0'); - bit_buffer.insert(bit_buffer.end(), repeat, current_bit); + for (uint8_t j = 0; j < repeat && bit_pos < MAX_BUFFER_SIZE; j++) { + bit_buffer[bit_pos++] = current_bit; + } current_bit = !current_bit; } current_bit = !current_bit; - if (string_buffer.length() < MAX_BUFFER_SIZE) { - uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length(); - string_buffer_2 += to_string(remain) + ", "; - string_buffer.append(remain, current_bit ? '1' : '0'); - bit_buffer.insert(bit_buffer.end(), remain, current_bit); + size_t bits_before_padding = bit_pos; + while (bit_pos < MAX_BUFFER_SIZE) { + bit_buffer[bit_pos++] = current_bit; } uint8_t tx20_sa = 0; @@ -108,8 +106,24 @@ void Tx20Component::decode_and_publish_() { // 2. Check received checksum matches calculated checksum // 3. Check that Wind Direction matches Wind Direction (Inverted) // 4. Check that Wind Speed matches Wind Speed (Inverted) - ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str()); - ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str()); +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE + // Build debug strings from completed data + char debug_buf[320]; // buffer values: max 40 entries * 7 chars each + size_t debug_pos = 0; + for (int i = 1; i <= max_buffer_index; i++) { + debug_pos = buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%u, ", this->store_.buffer[i]); + } + if (bits_before_padding < MAX_BUFFER_SIZE) { + buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%zu, ", MAX_BUFFER_SIZE - bits_before_padding); + } + char bits_buf[MAX_BUFFER_SIZE + 1]; + for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) { + bits_buf[i] = bit_buffer[i] ? '1' : '0'; + } + bits_buf[MAX_BUFFER_SIZE] = '\0'; + ESP_LOGVV(TAG, "BUFFER %s", debug_buf); + ESP_LOGVV(TAG, "Decoded bits %s", bits_buf); +#endif if (tx20_sa == 4) { if (chk == tx20_sd) { diff --git a/esphome/components/tx20/tx20.h b/esphome/components/tx20/tx20.h index 95a9517227..d1673f99f2 100644 --- a/esphome/components/tx20/tx20.h +++ b/esphome/components/tx20/tx20.h @@ -35,7 +35,6 @@ class Tx20Component : public Component { void setup() override; void dump_config() override; - float get_setup_priority() const override; void loop() override; protected: diff --git a/esphome/components/udp/__init__.py b/esphome/components/udp/__init__.py index 9be196d420..8252e35023 100644 --- a/esphome/components/udp/__init__.py +++ b/esphome/components/udp/__init__.py @@ -12,8 +12,8 @@ from esphome.components.packet_transport import ( ) import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID -from esphome.core import ID, Lambda -from esphome.cpp_generator import ExpressionStatement, MockObj +from esphome.core import ID +from esphome.cpp_generator import literal CODEOWNERS = ["@clydebarrow"] DEPENDENCIES = ["network"] @@ -24,6 +24,8 @@ udp_ns = cg.esphome_ns.namespace("udp") UDPComponent = udp_ns.class_("UDPComponent", cg.Component) UDPWriteAction = udp_ns.class_("UDPWriteAction", automation.Action) trigger_args = cg.std_vector.template(cg.uint8) +trigger_argname = "data" +trigger_argtype = [(trigger_args, trigger_argname)] CONF_ADDRESSES = "addresses" CONF_LISTEN_ADDRESS = "listen_address" @@ -111,13 +113,14 @@ async def to_code(config): cg.add(var.set_addresses([str(addr) for addr in config[CONF_ADDRESSES]])) if on_receive := config.get(CONF_ON_RECEIVE): on_receive = on_receive[0] - trigger = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) + trigger_id = cg.new_Pvariable(on_receive[CONF_TRIGGER_ID]) trigger = await automation.build_automation( - trigger, [(trigger_args, "data")], on_receive + trigger_id, trigger_argtype, on_receive ) - trigger = Lambda(str(ExpressionStatement(trigger.trigger(MockObj("data"))))) - trigger = await cg.process_lambda(trigger, [(trigger_args, "data")]) - cg.add(var.add_listener(trigger)) + trigger_lambda = await cg.process_lambda( + trigger.trigger(literal(trigger_argname)), trigger_argtype + ) + cg.add(var.add_listener(trigger_lambda)) cg.add(var.set_should_listen()) diff --git a/esphome/components/ultrasonic/__init__.py b/esphome/components/ultrasonic/__init__.py index 71a87b6ae5..3bca12bffc 100644 --- a/esphome/components/ultrasonic/__init__.py +++ b/esphome/components/ultrasonic/__init__.py @@ -1 +1 @@ -CODEOWNERS = ["@OttoWinter"] +CODEOWNERS = ["@swoboda1337", "@ssieb"] diff --git a/esphome/components/ultrasonic/sensor.py b/esphome/components/ultrasonic/sensor.py index 4b04ee7578..fad4e6b11d 100644 --- a/esphome/components/ultrasonic/sensor.py +++ b/esphome/components/ultrasonic/sensor.py @@ -34,7 +34,7 @@ CONFIG_SCHEMA = ( { cv.Required(CONF_TRIGGER_PIN): pins.internal_gpio_output_pin_schema, cv.Required(CONF_ECHO_PIN): pins.internal_gpio_input_pin_schema, - cv.Optional(CONF_TIMEOUT): cv.distance, + cv.Optional(CONF_TIMEOUT, default="2m"): cv.distance, cv.Optional( CONF_PULSE_TIME, default="10us" ): cv.positive_time_period_microseconds, @@ -52,12 +52,5 @@ async def to_code(config): cg.add(var.set_trigger_pin(trigger)) echo = await cg.gpio_pin_expression(config[CONF_ECHO_PIN]) cg.add(var.set_echo_pin(echo)) - - # Remove before 2026.8.0 - if CONF_TIMEOUT in config: - _LOGGER.warning( - "'timeout' option is deprecated and will be removed in 2026.8.0. " - "The option has no effect and can be safely removed." - ) - + cg.add(var.set_timeout_us(config[CONF_TIMEOUT] / (0.000343 / 2))) cg.add(var.set_pulse_time_us(config[CONF_PULSE_TIME])) diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.cpp b/esphome/components/ultrasonic/ultrasonic_sensor.cpp index 369a10edbd..d3f7e69444 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.cpp +++ b/esphome/components/ultrasonic/ultrasonic_sensor.cpp @@ -6,12 +6,11 @@ namespace esphome::ultrasonic { static const char *const TAG = "ultrasonic.sensor"; -static constexpr uint32_t DEBOUNCE_US = 50; // Ignore edges within 50us (noise filtering) -static constexpr uint32_t MEASUREMENT_TIMEOUT_US = 80000; // Maximum time to wait for measurement completion +static constexpr uint32_t START_TIMEOUT_US = 40000; // Maximum time to wait for echo pulse to start void IRAM_ATTR UltrasonicSensorStore::gpio_intr(UltrasonicSensorStore *arg) { uint32_t now = micros(); - if (!arg->echo_start || (now - arg->echo_start_us) <= DEBOUNCE_US) { + if (arg->echo_pin_isr.digital_read()) { arg->echo_start_us = now; arg->echo_start = true; } else { @@ -38,6 +37,7 @@ void UltrasonicSensorComponent::setup() { this->trigger_pin_->digital_write(false); this->trigger_pin_isr_ = this->trigger_pin_->to_isr(); this->echo_pin_->setup(); + this->store_.echo_pin_isr = this->echo_pin_->to_isr(); this->echo_pin_->attach_interrupt(UltrasonicSensorStore::gpio_intr, &this->store_, gpio::INTERRUPT_ANY_EDGE); } @@ -53,29 +53,55 @@ void UltrasonicSensorComponent::loop() { return; } + if (!this->store_.echo_start) { + uint32_t elapsed = micros() - this->measurement_start_us_; + if (elapsed >= START_TIMEOUT_US) { + ESP_LOGW(TAG, "'%s' - Measurement start timed out", this->name_.c_str()); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } else { + uint32_t elapsed; + if (this->store_.echo_end) { + elapsed = this->store_.echo_end_us - this->store_.echo_start_us; + } else { + elapsed = micros() - this->store_.echo_start_us; + } + if (elapsed >= this->timeout_us_) { + ESP_LOGD(TAG, "'%s' - Measurement pulse timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); + this->publish_state(NAN); + this->measurement_pending_ = false; + return; + } + } + if (this->store_.echo_end) { - uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; - ESP_LOGV(TAG, "Echo took %" PRIu32 "us", pulse_duration); - float result = UltrasonicSensorComponent::us_to_m(pulse_duration); - ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + float result; + if (this->store_.echo_start) { + uint32_t pulse_duration = this->store_.echo_end_us - this->store_.echo_start_us; + ESP_LOGV(TAG, "pulse start took %" PRIu32 "us, echo took %" PRIu32 "us", + this->store_.echo_start_us - this->measurement_start_us_, pulse_duration); + result = UltrasonicSensorComponent::us_to_m(pulse_duration); + ESP_LOGD(TAG, "'%s' - Got distance: %.3f m", this->name_.c_str(), result); + } else { + ESP_LOGW(TAG, "'%s' - pulse end before pulse start, does the echo pin need to be inverted?", this->name_.c_str()); + result = NAN; + } this->publish_state(result); this->measurement_pending_ = false; return; } - - uint32_t elapsed = micros() - this->measurement_start_us_; - if (elapsed >= MEASUREMENT_TIMEOUT_US) { - ESP_LOGD(TAG, "'%s' - Measurement timed out after %" PRIu32 "us", this->name_.c_str(), elapsed); - this->publish_state(NAN); - this->measurement_pending_ = false; - } } void UltrasonicSensorComponent::dump_config() { LOG_SENSOR("", "Ultrasonic Sensor", this); LOG_PIN(" Echo Pin: ", this->echo_pin_); LOG_PIN(" Trigger Pin: ", this->trigger_pin_); - ESP_LOGCONFIG(TAG, " Pulse time: %" PRIu32 " us", this->pulse_time_us_); + ESP_LOGCONFIG(TAG, + " Pulse time: %" PRIu32 " µs\n" + " Timeout: %" PRIu32 " µs", + this->pulse_time_us_, this->timeout_us_); LOG_UPDATE_INTERVAL(this); } diff --git a/esphome/components/ultrasonic/ultrasonic_sensor.h b/esphome/components/ultrasonic/ultrasonic_sensor.h index b0c00e51f0..541f7d2b70 100644 --- a/esphome/components/ultrasonic/ultrasonic_sensor.h +++ b/esphome/components/ultrasonic/ultrasonic_sensor.h @@ -11,6 +11,8 @@ namespace esphome::ultrasonic { struct UltrasonicSensorStore { static void gpio_intr(UltrasonicSensorStore *arg); + ISRInternalGPIOPin echo_pin_isr; + volatile uint32_t wait_start_us{0}; volatile uint32_t echo_start_us{0}; volatile uint32_t echo_end_us{0}; volatile bool echo_start{false}; @@ -27,8 +29,8 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent void dump_config() override; void update() override; - float get_setup_priority() const override { return setup_priority::DATA; } - + /// Set the maximum time in µs to wait for the echo to return + void set_timeout_us(uint32_t timeout_us) { this->timeout_us_ = timeout_us; } /// Set the time in µs the trigger pin should be enabled for in µs, defaults to 10µs (for HC-SR04) void set_pulse_time_us(uint32_t pulse_time_us) { this->pulse_time_us_ = pulse_time_us; } @@ -41,6 +43,7 @@ class UltrasonicSensorComponent : public sensor::Sensor, public PollingComponent ISRInternalGPIOPin trigger_pin_isr_; InternalGPIOPin *echo_pin_; UltrasonicSensorStore store_; + uint32_t timeout_us_{}; uint32_t pulse_time_us_{}; uint32_t measurement_start_us_{0}; diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 515e4c2c18..7edea2fe22 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -2,12 +2,21 @@ #include "esphome/core/defines.h" #include "esphome/core/controller_registry.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" namespace esphome { namespace update { static const char *const TAG = "update"; +// Update state strings indexed by UpdateState enum (0-3): UNKNOWN, NO UPDATE, UPDATE AVAILABLE, INSTALLING +PROGMEM_STRING_TABLE(UpdateStateStrings, "UNKNOWN", "NO UPDATE", "UPDATE AVAILABLE", "INSTALLING"); + +const LogString *update_state_to_string(UpdateState state) { + return UpdateStateStrings::get_log_str(static_cast(state), + static_cast(UpdateState::UPDATE_STATE_UNKNOWN)); +} + void UpdateEntity::publish_state() { ESP_LOGD(TAG, "'%s' >>\n" diff --git a/esphome/components/update/update_entity.h b/esphome/components/update/update_entity.h index 8eba78b44b..405346bee4 100644 --- a/esphome/components/update/update_entity.h +++ b/esphome/components/update/update_entity.h @@ -27,6 +27,8 @@ enum UpdateState : uint8_t { UPDATE_STATE_INSTALLING, }; +const LogString *update_state_to_string(UpdateState state); + class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { public: void publish_state(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp index 5c91150f30..224d6e3ab1 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm_esp32.cpp @@ -155,8 +155,9 @@ void USBCDCACMInstance::setup() { return; } - // Use a larger stack size for (very) verbose logging - const size_t stack_size = esp_log_level_get(TAG) > ESP_LOG_DEBUG ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; + // Use a larger stack size for very verbose logging + constexpr size_t stack_size = + ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE ? USB_TX_TASK_STACK_SIZE_VV : USB_TX_TASK_STACK_SIZE; // Create a simple, unique task name per interface char task_name[] = "usb_tx_0"; diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index 607f614ef7..493ffd8da2 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -23,17 +23,11 @@ const LogString *valve_command_to_str(float pos) { return LOG_STR("UNKNOWN"); } } +// Valve operation strings indexed by ValveOperation enum (0-2): IDLE, OPENING, CLOSING, plus UNKNOWN +PROGMEM_STRING_TABLE(ValveOperationStrings, "IDLE", "OPENING", "CLOSING", "UNKNOWN"); + const LogString *valve_operation_to_str(ValveOperation op) { - switch (op) { - case VALVE_OPERATION_IDLE: - return LOG_STR("IDLE"); - case VALVE_OPERATION_OPENING: - return LOG_STR("OPENING"); - case VALVE_OPERATION_CLOSING: - return LOG_STR("CLOSING"); - default: - return LOG_STR("UNKNOWN"); - } + return ValveOperationStrings::get_log_str(static_cast(op), ValveOperationStrings::LAST_INDEX); } Valve::Valve() : position{VALVE_OPEN} {} diff --git a/esphome/components/vbus/__init__.py b/esphome/components/vbus/__init__.py index d916d7c064..5790a9cce0 100644 --- a/esphome/components/vbus/__init__.py +++ b/esphome/components/vbus/__init__.py @@ -16,6 +16,7 @@ CONF_VBUS_ID = "vbus_id" CONF_DELTASOL_BS_PLUS = "deltasol_bs_plus" CONF_DELTASOL_BS_2009 = "deltasol_bs_2009" +CONF_DELTASOL_BS2 = "deltasol_bs2" CONF_DELTASOL_C = "deltasol_c" CONF_DELTASOL_CS2 = "deltasol_cs2" CONF_DELTASOL_CS_PLUS = "deltasol_cs_plus" diff --git a/esphome/components/vbus/binary_sensor/__init__.py b/esphome/components/vbus/binary_sensor/__init__.py index ae927656c0..70dda94300 100644 --- a/esphome/components/vbus/binary_sensor/__init__.py +++ b/esphome/components/vbus/binary_sensor/__init__.py @@ -15,6 +15,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -27,6 +28,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusBSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009BSensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2BSensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCBSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2BSensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusBSensor", cg.Component) @@ -118,6 +120,28 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_SENSOR1_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR2_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR3_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + cv.Optional(CONF_SENSOR4_ERROR): binary_sensor.binary_sensor_schema( + device_class=DEVICE_CLASS_PROBLEM, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -275,6 +299,23 @@ async def to_code(config): ) cg.add(var.set_frost_protection_active_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_SENSOR1_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR1_ERROR]) + cg.add(var.set_s1_error_bsensor(sens)) + if CONF_SENSOR2_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR2_ERROR]) + cg.add(var.set_s2_error_bsensor(sens)) + if CONF_SENSOR3_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR3_ERROR]) + cg.add(var.set_s3_error_bsensor(sens)) + if CONF_SENSOR4_ERROR in config: + sens = await binary_sensor.new_binary_sensor(config[CONF_SENSOR4_ERROR]) + cg.add(var.set_s4_error_bsensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp index 4ccd149935..c1d7bc1b18 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.cpp @@ -129,6 +129,25 @@ void DeltaSolCSPlusBSensor::handle_message(std::vector &message) { this->s4_error_bsensor_->publish_state(message[20] & 8); } +void DeltaSolBS2BSensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_BINARY_SENSOR(" ", "Sensor 1 Error", this->s1_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 2 Error", this->s2_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 3 Error", this->s3_error_bsensor_); + LOG_BINARY_SENSOR(" ", "Sensor 4 Error", this->s4_error_bsensor_); +} + +void DeltaSolBS2BSensor::handle_message(std::vector &message) { + if (this->s1_error_bsensor_ != nullptr) + this->s1_error_bsensor_->publish_state(message[10] & 1); + if (this->s2_error_bsensor_ != nullptr) + this->s2_error_bsensor_->publish_state(message[10] & 2); + if (this->s3_error_bsensor_ != nullptr) + this->s3_error_bsensor_->publish_state(message[10] & 4); + if (this->s4_error_bsensor_ != nullptr) + this->s4_error_bsensor_->publish_state(message[10] & 8); +} + void VBusCustomBSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Binary Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h index 146aa1b673..2decdde602 100644 --- a/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h +++ b/esphome/components/vbus/binary_sensor/vbus_binary_sensor.h @@ -111,6 +111,23 @@ class DeltaSolCSPlusBSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2BSensor : public VBusListener, public Component { + public: + void dump_config() override; + void set_s1_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s1_error_bsensor_ = bsensor; } + void set_s2_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s2_error_bsensor_ = bsensor; } + void set_s3_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s3_error_bsensor_ = bsensor; } + void set_s4_error_bsensor(binary_sensor::BinarySensor *bsensor) { this->s4_error_bsensor_ = bsensor; } + + protected: + binary_sensor::BinarySensor *s1_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s2_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s3_error_bsensor_{nullptr}; + binary_sensor::BinarySensor *s4_error_bsensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class VBusCustomSubBSensor; class VBusCustomBSensor : public VBusListener, public Component { diff --git a/esphome/components/vbus/sensor/__init__.py b/esphome/components/vbus/sensor/__init__.py index fcff698ac0..ff8ef98a1a 100644 --- a/esphome/components/vbus/sensor/__init__.py +++ b/esphome/components/vbus/sensor/__init__.py @@ -31,6 +31,7 @@ from esphome.const import ( ) from .. import ( + CONF_DELTASOL_BS2, CONF_DELTASOL_BS_2009, CONF_DELTASOL_BS_PLUS, CONF_DELTASOL_C, @@ -43,6 +44,7 @@ from .. import ( DeltaSol_BS_Plus = vbus_ns.class_("DeltaSolBSPlusSensor", cg.Component) DeltaSol_BS_2009 = vbus_ns.class_("DeltaSolBS2009Sensor", cg.Component) +DeltaSol_BS2 = vbus_ns.class_("DeltaSolBS2Sensor", cg.Component) DeltaSol_C = vbus_ns.class_("DeltaSolCSensor", cg.Component) DeltaSol_CS2 = vbus_ns.class_("DeltaSolCS2Sensor", cg.Component) DeltaSol_CS_Plus = vbus_ns.class_("DeltaSolCSPlusSensor", cg.Component) @@ -227,6 +229,79 @@ CONFIG_SCHEMA = cv.typed_schema( ), } ), + CONF_DELTASOL_BS2: cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(DeltaSol_BS2), + cv.GenerateID(CONF_VBUS_ID): cv.use_id(VBus), + cv.Optional(CONF_TEMPERATURE_1): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_2): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_3): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_TEMPERATURE_4): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + icon=ICON_THERMOMETER, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_1): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_PUMP_SPEED_2): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + icon=ICON_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_EMPTY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_1): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_OPERATING_HOURS_2): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + icon=ICON_TIMER, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_HEAT_QUANTITY): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT_HOURS, + icon=ICON_RADIATOR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_VERSION): sensor.sensor_schema( + accuracy_decimals=2, + entity_category=ENTITY_CATEGORY_DIAGNOSTIC, + ), + } + ), CONF_DELTASOL_C: cv.COMPONENT_SCHEMA.extend( { cv.GenerateID(): cv.declare_id(DeltaSol_C), @@ -560,6 +635,41 @@ async def to_code(config): sens = await sensor.new_sensor(config[CONF_VERSION]) cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_BS2: + cg.add(var.set_command(0x0100)) + cg.add(var.set_source(0x4278)) + cg.add(var.set_dest(0x0010)) + if CONF_TEMPERATURE_1 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_1]) + cg.add(var.set_temperature1_sensor(sens)) + if CONF_TEMPERATURE_2 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_2]) + cg.add(var.set_temperature2_sensor(sens)) + if CONF_TEMPERATURE_3 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_3]) + cg.add(var.set_temperature3_sensor(sens)) + if CONF_TEMPERATURE_4 in config: + sens = await sensor.new_sensor(config[CONF_TEMPERATURE_4]) + cg.add(var.set_temperature4_sensor(sens)) + if CONF_PUMP_SPEED_1 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_1]) + cg.add(var.set_pump_speed1_sensor(sens)) + if CONF_PUMP_SPEED_2 in config: + sens = await sensor.new_sensor(config[CONF_PUMP_SPEED_2]) + cg.add(var.set_pump_speed2_sensor(sens)) + if CONF_OPERATING_HOURS_1 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_1]) + cg.add(var.set_operating_hours1_sensor(sens)) + if CONF_OPERATING_HOURS_2 in config: + sens = await sensor.new_sensor(config[CONF_OPERATING_HOURS_2]) + cg.add(var.set_operating_hours2_sensor(sens)) + if CONF_HEAT_QUANTITY in config: + sens = await sensor.new_sensor(config[CONF_HEAT_QUANTITY]) + cg.add(var.set_heat_quantity_sensor(sens)) + if CONF_VERSION in config: + sens = await sensor.new_sensor(config[CONF_VERSION]) + cg.add(var.set_version_sensor(sens)) + elif config[CONF_MODEL] == CONF_DELTASOL_C: cg.add(var.set_command(0x0100)) cg.add(var.set_source(0x4212)) diff --git a/esphome/components/vbus/sensor/vbus_sensor.cpp b/esphome/components/vbus/sensor/vbus_sensor.cpp index e81c0486d4..75c9ea1aee 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.cpp +++ b/esphome/components/vbus/sensor/vbus_sensor.cpp @@ -214,6 +214,45 @@ void DeltaSolCSPlusSensor::handle_message(std::vector &message) { this->flow_rate_sensor_->publish_state(get_u16(message, 38)); } +void DeltaSolBS2Sensor::dump_config() { + ESP_LOGCONFIG(TAG, "DeltaSol BS/2 (DrainBack):"); + LOG_SENSOR(" ", "Temperature 1", this->temperature1_sensor_); + LOG_SENSOR(" ", "Temperature 2", this->temperature2_sensor_); + LOG_SENSOR(" ", "Temperature 3", this->temperature3_sensor_); + LOG_SENSOR(" ", "Temperature 4", this->temperature4_sensor_); + LOG_SENSOR(" ", "Pump Speed 1", this->pump_speed1_sensor_); + LOG_SENSOR(" ", "Pump Speed 2", this->pump_speed2_sensor_); + LOG_SENSOR(" ", "Operating Hours 1", this->operating_hours1_sensor_); + LOG_SENSOR(" ", "Operating Hours 2", this->operating_hours2_sensor_); + LOG_SENSOR(" ", "Heat Quantity", this->heat_quantity_sensor_); + LOG_SENSOR(" ", "FW Version", this->version_sensor_); +} + +void DeltaSolBS2Sensor::handle_message(std::vector &message) { + if (this->temperature1_sensor_ != nullptr) + this->temperature1_sensor_->publish_state(get_i16(message, 0) * 0.1f); + if (this->temperature2_sensor_ != nullptr) + this->temperature2_sensor_->publish_state(get_i16(message, 2) * 0.1f); + if (this->temperature3_sensor_ != nullptr) + this->temperature3_sensor_->publish_state(get_i16(message, 4) * 0.1f); + if (this->temperature4_sensor_ != nullptr) + this->temperature4_sensor_->publish_state(get_i16(message, 6) * 0.1f); + if (this->pump_speed1_sensor_ != nullptr) + this->pump_speed1_sensor_->publish_state(message[8]); + if (this->pump_speed2_sensor_ != nullptr) + this->pump_speed2_sensor_->publish_state(message[9]); + if (this->operating_hours1_sensor_ != nullptr) + this->operating_hours1_sensor_->publish_state(get_u16(message, 12)); + if (this->operating_hours2_sensor_ != nullptr) + this->operating_hours2_sensor_->publish_state(get_u16(message, 14)); + if (this->heat_quantity_sensor_ != nullptr) { + float heat_wh = get_u16(message, 16) + get_u16(message, 18) * 1000.0f + get_u16(message, 20) * 1000000.0f; + this->heat_quantity_sensor_->publish_state(heat_wh); + } + if (this->version_sensor_ != nullptr) + this->version_sensor_->publish_state(get_u16(message, 24) * 0.01f); +} + void VBusCustomSensor::dump_config() { ESP_LOGCONFIG(TAG, "VBus Custom Sensor:"); if (this->source_ == 0xffff) { diff --git a/esphome/components/vbus/sensor/vbus_sensor.h b/esphome/components/vbus/sensor/vbus_sensor.h index d5535b2019..cea2ee1c86 100644 --- a/esphome/components/vbus/sensor/vbus_sensor.h +++ b/esphome/components/vbus/sensor/vbus_sensor.h @@ -157,6 +157,36 @@ class DeltaSolCSPlusSensor : public VBusListener, public Component { void handle_message(std::vector &message) override; }; +class DeltaSolBS2Sensor : public VBusListener, public Component { + public: + void dump_config() override; + + void set_temperature1_sensor(sensor::Sensor *sensor) { this->temperature1_sensor_ = sensor; } + void set_temperature2_sensor(sensor::Sensor *sensor) { this->temperature2_sensor_ = sensor; } + void set_temperature3_sensor(sensor::Sensor *sensor) { this->temperature3_sensor_ = sensor; } + void set_temperature4_sensor(sensor::Sensor *sensor) { this->temperature4_sensor_ = sensor; } + void set_pump_speed1_sensor(sensor::Sensor *sensor) { this->pump_speed1_sensor_ = sensor; } + void set_pump_speed2_sensor(sensor::Sensor *sensor) { this->pump_speed2_sensor_ = sensor; } + void set_operating_hours1_sensor(sensor::Sensor *sensor) { this->operating_hours1_sensor_ = sensor; } + void set_operating_hours2_sensor(sensor::Sensor *sensor) { this->operating_hours2_sensor_ = sensor; } + void set_heat_quantity_sensor(sensor::Sensor *sensor) { this->heat_quantity_sensor_ = sensor; } + void set_version_sensor(sensor::Sensor *sensor) { this->version_sensor_ = sensor; } + + protected: + sensor::Sensor *temperature1_sensor_{nullptr}; + sensor::Sensor *temperature2_sensor_{nullptr}; + sensor::Sensor *temperature3_sensor_{nullptr}; + sensor::Sensor *temperature4_sensor_{nullptr}; + sensor::Sensor *pump_speed1_sensor_{nullptr}; + sensor::Sensor *pump_speed2_sensor_{nullptr}; + sensor::Sensor *operating_hours1_sensor_{nullptr}; + sensor::Sensor *operating_hours2_sensor_{nullptr}; + sensor::Sensor *heat_quantity_sensor_{nullptr}; + sensor::Sensor *version_sensor_{nullptr}; + + void handle_message(std::vector &message) override; +}; + class VBusCustomSubSensor; class VBusCustomSensor : public VBusListener, public Component { diff --git a/esphome/components/version/version_text_sensor.cpp b/esphome/components/version/version_text_sensor.cpp index 584b8abfb2..2e5686008b 100644 --- a/esphome/components/version/version_text_sensor.cpp +++ b/esphome/components/version/version_text_sensor.cpp @@ -32,7 +32,6 @@ void VersionTextSensor::setup() { version_str[sizeof(version_str) - 1] = '\0'; this->publish_state(version_str); } -float VersionTextSensor::get_setup_priority() const { return setup_priority::DATA; } void VersionTextSensor::set_hide_timestamp(bool hide_timestamp) { this->hide_timestamp_ = hide_timestamp; } void VersionTextSensor::dump_config() { LOG_TEXT_SENSOR("", "Version Text Sensor", this); } diff --git a/esphome/components/version/version_text_sensor.h b/esphome/components/version/version_text_sensor.h index 6813da7830..b7d8001120 100644 --- a/esphome/components/version/version_text_sensor.h +++ b/esphome/components/version/version_text_sensor.h @@ -11,7 +11,6 @@ class VersionTextSensor : public text_sensor::TextSensor, public Component { void set_hide_timestamp(bool hide_timestamp); void setup() override; void dump_config() override; - float get_setup_priority() const override; protected: bool hide_timestamp_{false}; diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index e2516d5fb8..7f5fbe62e1 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -197,7 +197,7 @@ void VoiceAssistant::loop() { switch (this->state_) { case State::IDLE: { if (this->continuous_ && this->desired_state_ == State::IDLE) { - this->idle_trigger_->trigger(); + this->idle_trigger_.trigger(); this->set_state_(State::START_MICROPHONE, State::START_PIPELINE); } else { this->deallocate_buffers_(); @@ -254,7 +254,7 @@ void VoiceAssistant::loop() { if (this->api_client_ == nullptr || !this->api_client_->send_message(msg, api::VoiceAssistantRequest::MESSAGE_TYPE)) { ESP_LOGW(TAG, "Could not request start"); - this->error_trigger_->trigger("not-connected", "Could not request start"); + this->error_trigger_.trigger("not-connected", "Could not request start"); this->continuous_ = false; this->set_state_(State::IDLE, State::IDLE); break; @@ -384,7 +384,7 @@ void VoiceAssistant::loop() { this->wait_for_stream_end_ = false; this->stream_ended_ = false; - this->tts_stream_end_trigger_->trigger(); + this->tts_stream_end_trigger_.trigger(); } #endif if (this->continue_conversation_) { @@ -425,7 +425,7 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr return; } this->api_client_ = nullptr; - this->client_disconnected_trigger_->trigger(); + this->client_disconnected_trigger_.trigger(); return; } @@ -440,7 +440,7 @@ void VoiceAssistant::client_subscription(api::APIConnection *client, bool subscr } this->api_client_ = client; - this->client_connected_trigger_->trigger(); + this->client_connected_trigger_.trigger(); } static const LogString *voice_assistant_state_to_string(State state) { @@ -491,7 +491,7 @@ void VoiceAssistant::set_state_(State state, State desired_state) { void VoiceAssistant::failed_to_start() { ESP_LOGE(TAG, "Failed to start server. See Home Assistant logs for more details."); - this->error_trigger_->trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); + this->error_trigger_.trigger("failed-to-start", "Failed to start server. See Home Assistant logs for more details."); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } @@ -637,18 +637,18 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } #endif - this->defer([this]() { this->start_trigger_->trigger(); }); + this->defer([this]() { this->start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_START: break; case api::enums::VOICE_ASSISTANT_WAKE_WORD_END: { ESP_LOGD(TAG, "Wake word detected"); - this->defer([this]() { this->wake_word_detected_trigger_->trigger(); }); + this->defer([this]() { this->wake_word_detected_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_STT_START: ESP_LOGD(TAG, "STT started"); - this->defer([this]() { this->listening_trigger_->trigger(); }); + this->defer([this]() { this->listening_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_END: { std::string text; @@ -665,12 +665,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { text += "..."; } ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str()); - this->defer([this, text]() { this->stt_end_trigger_->trigger(text); }); + this->defer([this, text]() { this->stt_end_trigger_.trigger(text); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_START: ESP_LOGD(TAG, "Intent started"); - this->defer([this]() { this->intent_start_trigger_->trigger(); }); + this->defer([this]() { this->intent_start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: { ESP_LOGD(TAG, "Intent progress"); @@ -693,7 +693,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } } #endif - this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); }); + this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_.trigger(tts_url_for_trigger); }); break; } case api::enums::VOICE_ASSISTANT_INTENT_END: { @@ -704,7 +704,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->continue_conversation_ = (arg.value == "1"); } } - this->defer([this]() { this->intent_end_trigger_->trigger(); }); + this->defer([this]() { this->intent_end_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_TTS_START: { @@ -724,7 +724,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } ESP_LOGD(TAG, "Response: \"%s\"", text.c_str()); this->defer([this, text]() { - this->tts_start_trigger_->trigger(text); + this->tts_start_trigger_.trigger(text); #ifdef USE_SPEAKER if (this->speaker_ != nullptr) { this->speaker_->start(); @@ -756,7 +756,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } this->started_streaming_tts_ = false; // Helps indicate reaching the TTS_END stage #endif - this->tts_end_trigger_->trigger(url); + this->tts_end_trigger_.trigger(url); }); State new_state = this->local_output_ ? State::STREAMING_RESPONSE : State::IDLE; if (new_state != this->state_) { @@ -776,7 +776,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { // No TTS start event ("nevermind") this->set_state_(State::IDLE, State::IDLE); } - this->defer([this]() { this->end_trigger_->trigger(); }); + this->defer([this]() { this->end_trigger_.trigger(); }); break; } case api::enums::VOICE_ASSISTANT_ERROR: { @@ -796,7 +796,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { // Wake word is not set up or not ready on Home Assistant so stop and do not retry until user starts again. this->defer([this, code, message]() { this->request_stop(); - this->error_trigger_->trigger(code, message); + this->error_trigger_.trigger(code, message); }); return; } @@ -805,7 +805,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { this->signal_stop_(); this->set_state_(State::STOP_MICROPHONE, State::IDLE); } - this->defer([this, code, message]() { this->error_trigger_->trigger(code, message); }); + this->defer([this, code, message]() { this->error_trigger_.trigger(code, message); }); break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { @@ -813,7 +813,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { if (this->speaker_ != nullptr) { this->wait_for_stream_end_ = true; ESP_LOGD(TAG, "TTS stream start"); - this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + this->defer([this] { this->tts_stream_start_trigger_.trigger(); }); } #endif break; @@ -829,12 +829,12 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_STT_VAD_START: ESP_LOGD(TAG, "Starting STT by VAD"); - this->defer([this]() { this->stt_vad_start_trigger_->trigger(); }); + this->defer([this]() { this->stt_vad_start_trigger_.trigger(); }); break; case api::enums::VOICE_ASSISTANT_STT_VAD_END: ESP_LOGD(TAG, "STT by VAD end"); this->set_state_(State::STOP_MICROPHONE, State::AWAITING_RESPONSE); - this->defer([this]() { this->stt_vad_end_trigger_->trigger(); }); + this->defer([this]() { this->stt_vad_end_trigger_.trigger(); }); break; default: ESP_LOGD(TAG, "Unhandled event type: %" PRId32, msg.event_type); @@ -876,17 +876,17 @@ void VoiceAssistant::on_timer_event(const api::VoiceAssistantTimerEventResponse switch (msg.event_type) { case api::enums::VOICE_ASSISTANT_TIMER_STARTED: - this->timer_started_trigger_->trigger(timer); + this->timer_started_trigger_.trigger(timer); break; case api::enums::VOICE_ASSISTANT_TIMER_UPDATED: - this->timer_updated_trigger_->trigger(timer); + this->timer_updated_trigger_.trigger(timer); break; case api::enums::VOICE_ASSISTANT_TIMER_CANCELLED: - this->timer_cancelled_trigger_->trigger(timer); + this->timer_cancelled_trigger_.trigger(timer); this->timers_.erase(timer.id); break; case api::enums::VOICE_ASSISTANT_TIMER_FINISHED: - this->timer_finished_trigger_->trigger(timer); + this->timer_finished_trigger_.trigger(timer); this->timers_.erase(timer.id); break; } @@ -910,13 +910,13 @@ void VoiceAssistant::timer_tick_() { } res.push_back(timer); } - this->timer_tick_trigger_->trigger(res); + this->timer_tick_trigger_.trigger(res); } void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) { #ifdef USE_MEDIA_PLAYER if (this->media_player_ != nullptr) { - this->tts_start_trigger_->trigger(msg.text); + this->tts_start_trigger_.trigger(msg.text); this->media_player_response_state_ = MediaPlayerResponseState::URL_SENT; @@ -939,8 +939,8 @@ void VoiceAssistant::on_announce(const api::VoiceAssistantAnnounceRequest &msg) this->set_state_(State::STREAMING_RESPONSE, State::STREAMING_RESPONSE); } - this->tts_end_trigger_->trigger(msg.media_id); - this->end_trigger_->trigger(); + this->tts_end_trigger_.trigger(msg.media_id); + this->end_trigger_.trigger(); } #endif } diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index d61a8fbbc1..2a5f3a55a7 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -195,38 +195,38 @@ class VoiceAssistant : public Component { void set_conversation_timeout(uint32_t conversation_timeout) { this->conversation_timeout_ = conversation_timeout; } void reset_conversation_id(); - Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; } - Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; } - Trigger *get_intent_progress_trigger() const { return this->intent_progress_trigger_; } - Trigger<> *get_listening_trigger() const { return this->listening_trigger_; } - Trigger<> *get_end_trigger() const { return this->end_trigger_; } - Trigger<> *get_start_trigger() const { return this->start_trigger_; } - Trigger<> *get_stt_vad_end_trigger() const { return this->stt_vad_end_trigger_; } - Trigger<> *get_stt_vad_start_trigger() const { return this->stt_vad_start_trigger_; } + Trigger<> *get_intent_end_trigger() { return &this->intent_end_trigger_; } + Trigger<> *get_intent_start_trigger() { return &this->intent_start_trigger_; } + Trigger *get_intent_progress_trigger() { return &this->intent_progress_trigger_; } + Trigger<> *get_listening_trigger() { return &this->listening_trigger_; } + Trigger<> *get_end_trigger() { return &this->end_trigger_; } + Trigger<> *get_start_trigger() { return &this->start_trigger_; } + Trigger<> *get_stt_vad_end_trigger() { return &this->stt_vad_end_trigger_; } + Trigger<> *get_stt_vad_start_trigger() { return &this->stt_vad_start_trigger_; } #ifdef USE_SPEAKER - Trigger<> *get_tts_stream_start_trigger() const { return this->tts_stream_start_trigger_; } - Trigger<> *get_tts_stream_end_trigger() const { return this->tts_stream_end_trigger_; } + Trigger<> *get_tts_stream_start_trigger() { return &this->tts_stream_start_trigger_; } + Trigger<> *get_tts_stream_end_trigger() { return &this->tts_stream_end_trigger_; } #endif - Trigger<> *get_wake_word_detected_trigger() const { return this->wake_word_detected_trigger_; } - Trigger *get_stt_end_trigger() const { return this->stt_end_trigger_; } - Trigger *get_tts_end_trigger() const { return this->tts_end_trigger_; } - Trigger *get_tts_start_trigger() const { return this->tts_start_trigger_; } - Trigger *get_error_trigger() const { return this->error_trigger_; } - Trigger<> *get_idle_trigger() const { return this->idle_trigger_; } + Trigger<> *get_wake_word_detected_trigger() { return &this->wake_word_detected_trigger_; } + Trigger *get_stt_end_trigger() { return &this->stt_end_trigger_; } + Trigger *get_tts_end_trigger() { return &this->tts_end_trigger_; } + Trigger *get_tts_start_trigger() { return &this->tts_start_trigger_; } + Trigger *get_error_trigger() { return &this->error_trigger_; } + Trigger<> *get_idle_trigger() { return &this->idle_trigger_; } - Trigger<> *get_client_connected_trigger() const { return this->client_connected_trigger_; } - Trigger<> *get_client_disconnected_trigger() const { return this->client_disconnected_trigger_; } + Trigger<> *get_client_connected_trigger() { return &this->client_connected_trigger_; } + Trigger<> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; } void client_subscription(api::APIConnection *client, bool subscribe); api::APIConnection *get_api_connection() const { return this->api_client_; } void set_wake_word(const std::string &wake_word) { this->wake_word_ = wake_word; } - Trigger *get_timer_started_trigger() const { return this->timer_started_trigger_; } - Trigger *get_timer_updated_trigger() const { return this->timer_updated_trigger_; } - Trigger *get_timer_cancelled_trigger() const { return this->timer_cancelled_trigger_; } - Trigger *get_timer_finished_trigger() const { return this->timer_finished_trigger_; } - Trigger> *get_timer_tick_trigger() const { return this->timer_tick_trigger_; } + Trigger *get_timer_started_trigger() { return &this->timer_started_trigger_; } + Trigger *get_timer_updated_trigger() { return &this->timer_updated_trigger_; } + Trigger *get_timer_cancelled_trigger() { return &this->timer_cancelled_trigger_; } + Trigger *get_timer_finished_trigger() { return &this->timer_finished_trigger_; } + Trigger> *get_timer_tick_trigger() { return &this->timer_tick_trigger_; } void set_has_timers(bool has_timers) { this->has_timers_ = has_timers; } const std::unordered_map &get_timers() const { return this->timers_; } @@ -243,37 +243,37 @@ class VoiceAssistant : public Component { std::unique_ptr socket_ = nullptr; struct sockaddr_storage dest_addr_; - Trigger<> *intent_end_trigger_ = new Trigger<>(); - Trigger<> *intent_start_trigger_ = new Trigger<>(); - Trigger<> *listening_trigger_ = new Trigger<>(); - Trigger<> *end_trigger_ = new Trigger<>(); - Trigger<> *start_trigger_ = new Trigger<>(); - Trigger<> *stt_vad_start_trigger_ = new Trigger<>(); - Trigger<> *stt_vad_end_trigger_ = new Trigger<>(); + Trigger<> intent_end_trigger_; + Trigger<> intent_start_trigger_; + Trigger<> listening_trigger_; + Trigger<> end_trigger_; + Trigger<> start_trigger_; + Trigger<> stt_vad_start_trigger_; + Trigger<> stt_vad_end_trigger_; #ifdef USE_SPEAKER - Trigger<> *tts_stream_start_trigger_ = new Trigger<>(); - Trigger<> *tts_stream_end_trigger_ = new Trigger<>(); + Trigger<> tts_stream_start_trigger_; + Trigger<> tts_stream_end_trigger_; #endif - Trigger *intent_progress_trigger_ = new Trigger(); - Trigger<> *wake_word_detected_trigger_ = new Trigger<>(); - Trigger *stt_end_trigger_ = new Trigger(); - Trigger *tts_end_trigger_ = new Trigger(); - Trigger *tts_start_trigger_ = new Trigger(); - Trigger *error_trigger_ = new Trigger(); - Trigger<> *idle_trigger_ = new Trigger<>(); + Trigger intent_progress_trigger_; + Trigger<> wake_word_detected_trigger_; + Trigger stt_end_trigger_; + Trigger tts_end_trigger_; + Trigger tts_start_trigger_; + Trigger error_trigger_; + Trigger<> idle_trigger_; - Trigger<> *client_connected_trigger_ = new Trigger<>(); - Trigger<> *client_disconnected_trigger_ = new Trigger<>(); + Trigger<> client_connected_trigger_; + Trigger<> client_disconnected_trigger_; api::APIConnection *api_client_{nullptr}; std::unordered_map timers_; void timer_tick_(); - Trigger *timer_started_trigger_ = new Trigger(); - Trigger *timer_finished_trigger_ = new Trigger(); - Trigger *timer_updated_trigger_ = new Trigger(); - Trigger *timer_cancelled_trigger_ = new Trigger(); - Trigger> *timer_tick_trigger_ = new Trigger>(); + Trigger timer_started_trigger_; + Trigger timer_finished_trigger_; + Trigger timer_updated_trigger_; + Trigger timer_cancelled_trigger_; + Trigger> timer_tick_trigger_; bool has_timers_{false}; bool timer_tick_running_{false}; diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index 7266294d84..e6d1562352 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -2,6 +2,7 @@ #include "esphome/core/log.h" #include "esphome/core/application.h" #include "esphome/core/controller_registry.h" +#include "esphome/core/progmem.h" #include @@ -22,23 +23,23 @@ WaterHeaterCall &WaterHeaterCall::set_mode(WaterHeaterMode mode) { return *this; } -WaterHeaterCall &WaterHeaterCall::set_mode(const std::string &mode) { - if (str_equals_case_insensitive(mode, "OFF")) { +WaterHeaterCall &WaterHeaterCall::set_mode(const char *mode) { + if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("OFF")) == 0) { this->set_mode(WATER_HEATER_MODE_OFF); - } else if (str_equals_case_insensitive(mode, "ECO")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ECO")) == 0) { this->set_mode(WATER_HEATER_MODE_ECO); - } else if (str_equals_case_insensitive(mode, "ELECTRIC")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("ELECTRIC")) == 0) { this->set_mode(WATER_HEATER_MODE_ELECTRIC); - } else if (str_equals_case_insensitive(mode, "PERFORMANCE")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("PERFORMANCE")) == 0) { this->set_mode(WATER_HEATER_MODE_PERFORMANCE); - } else if (str_equals_case_insensitive(mode, "HIGH_DEMAND")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HIGH_DEMAND")) == 0) { this->set_mode(WATER_HEATER_MODE_HIGH_DEMAND); - } else if (str_equals_case_insensitive(mode, "HEAT_PUMP")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("HEAT_PUMP")) == 0) { this->set_mode(WATER_HEATER_MODE_HEAT_PUMP); - } else if (str_equals_case_insensitive(mode, "GAS")) { + } else if (ESPHOME_strcasecmp_P(mode, ESPHOME_PSTR("GAS")) == 0) { this->set_mode(WATER_HEATER_MODE_GAS); } else { - ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str()); + ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode); } return *this; } @@ -232,25 +233,13 @@ void WaterHeater::set_visual_target_temperature_step_override(float visual_targe } #endif +// Water heater mode strings indexed by WaterHeaterMode enum (0-6): OFF, ECO, ELECTRIC, PERFORMANCE, HIGH_DEMAND, +// HEAT_PUMP, GAS +PROGMEM_STRING_TABLE(WaterHeaterModeStrings, "OFF", "ECO", "ELECTRIC", "PERFORMANCE", "HIGH_DEMAND", "HEAT_PUMP", "GAS", + "UNKNOWN"); + const LogString *water_heater_mode_to_string(WaterHeaterMode mode) { - switch (mode) { - case WATER_HEATER_MODE_OFF: - return LOG_STR("OFF"); - case WATER_HEATER_MODE_ECO: - return LOG_STR("ECO"); - case WATER_HEATER_MODE_ELECTRIC: - return LOG_STR("ELECTRIC"); - case WATER_HEATER_MODE_PERFORMANCE: - return LOG_STR("PERFORMANCE"); - case WATER_HEATER_MODE_HIGH_DEMAND: - return LOG_STR("HIGH_DEMAND"); - case WATER_HEATER_MODE_HEAT_PUMP: - return LOG_STR("HEAT_PUMP"); - case WATER_HEATER_MODE_GAS: - return LOG_STR("GAS"); - default: - return LOG_STR("UNKNOWN"); - } + return WaterHeaterModeStrings::get_log_str(static_cast(mode), WaterHeaterModeStrings::LAST_INDEX); } void WaterHeater::dump_traits_(const char *tag) { diff --git a/esphome/components/water_heater/water_heater.h b/esphome/components/water_heater/water_heater.h index 84fc46d208..7bd05ba7f5 100644 --- a/esphome/components/water_heater/water_heater.h +++ b/esphome/components/water_heater/water_heater.h @@ -75,7 +75,8 @@ class WaterHeaterCall { WaterHeaterCall(WaterHeater *parent); WaterHeaterCall &set_mode(WaterHeaterMode mode); - WaterHeaterCall &set_mode(const std::string &mode); + WaterHeaterCall &set_mode(const char *mode); + WaterHeaterCall &set_mode(const std::string &mode) { return this->set_mode(mode.c_str()); } WaterHeaterCall &set_target_temperature(float temperature); WaterHeaterCall &set_target_temperature_low(float temperature); WaterHeaterCall &set_target_temperature_high(float temperature); diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index d30cb524f4..dfd602be6b 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -29,6 +29,10 @@ #include "esphome/components/climate/climate.h" #endif +#ifdef USE_UPDATE +#include "esphome/components/update/update_entity.h" +#endif + #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif @@ -454,7 +458,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { #ifdef USE_WEBSERVER_PRIVATE_NETWORK_ACCESS void WebServer::handle_pna_cors_request(AsyncWebServerRequest *request) { - AsyncWebServerResponse *response = request->beginResponse(200, ""); + AsyncWebServerResponse *response = request->beginResponse(200, ESPHOME_F("")); response->addHeader(ESPHOME_F("Access-Control-Allow-Private-Network"), ESPHOME_F("true")); response->addHeader(ESPHOME_F("Private-Network-Access-Name"), App.get_name().c_str()); char mac_s[18]; @@ -721,11 +725,7 @@ void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlM } if (action != SWITCH_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_switch_action(obj, action); -#else this->defer([obj, action]() { execute_switch_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -1645,11 +1645,7 @@ void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMat } if (action != LOCK_ACTION_NONE) { -#ifdef USE_ESP8266 - execute_lock_action(obj, action); -#else this->defer([obj, action]() { execute_lock_action(obj, action); }); -#endif request->send(200); } else { request->send(404); @@ -1975,7 +1971,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Only allow transmit if the device supports it if (!obj->has_transmitter()) { - request->send(400, ESPHOME_F("text/plain"), "Device does not support transmission"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Device does not support transmission")); return; } @@ -2001,7 +1997,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Parse base64url-encoded raw timings (required) // Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping) if (!request->hasParam(ESPHOME_F("data"))) { - request->send(400, ESPHOME_F("text/plain"), "Missing 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter")); return; } @@ -2011,23 +2007,18 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur // Validate base64url is not empty if (encoded.empty()) { - request->send(400, ESPHOME_F("text/plain"), "Empty 'data' parameter"); + request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter")); return; } -#ifdef USE_ESP8266 - // ESP8266 is single-threaded, call directly - call.set_raw_timings_base64url(encoded); - call.perform(); -#else // Defer to main loop for thread safety. Move encoded string into lambda to ensure // it outlives the call - set_raw_timings_base64url stores a pointer, so the string // must remain valid until perform() completes. + // ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context. this->defer([call, encoded = std::move(encoded)]() mutable { call.set_raw_timings_base64url(encoded); call.perform(); }); -#endif request->send(200); return; @@ -2117,19 +2108,6 @@ std::string WebServer::event_json_(event::Event *obj, StringRef event_type, Json #endif #ifdef USE_UPDATE -static const LogString *update_state_to_string(update::UpdateState state) { - switch (state) { - case update::UPDATE_STATE_NO_UPDATE: - return LOG_STR("NO UPDATE"); - case update::UPDATE_STATE_AVAILABLE: - return LOG_STR("UPDATE AVAILABLE"); - case update::UPDATE_STATE_INSTALLING: - return LOG_STR("INSTALLING"); - default: - return LOG_STR("UNKNOWN"); - } -} - void WebServer::on_update(update::UpdateEntity *obj) { this->events_.deferrable_send_state(obj, "state", update_state_json_generator); } @@ -2171,7 +2149,7 @@ std::string WebServer::update_json_(update::UpdateEntity *obj, JsonDetail start_ JsonObject root = builder.root(); char buf[PSTR_LOCAL_SIZE]; - set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update_state_to_string(obj->state)), + set_json_icon_state_value(root, obj, "update", PSTR_LOCAL(update::update_state_to_string(obj->state)), obj->update_info.latest_version, start_config); if (start_config == DETAIL_ALL) { root[ESPHOME_F("current_version")] = obj->update_info.current_version; @@ -2485,7 +2463,7 @@ void WebServer::handleRequest(AsyncWebServerRequest *request) { else { // No matching handler found - send 404 ESP_LOGV(TAG, "Request for unknown URL: %s", url.c_str()); - request->send(404, "text/plain", "Not Found"); + request->send(404, ESPHOME_F("text/plain"), ESPHOME_F("Not Found")); } } diff --git a/esphome/components/web_server/web_server.h b/esphome/components/web_server/web_server.h index 92a5c7edee..224c051ece 100644 --- a/esphome/components/web_server/web_server.h +++ b/esphome/components/web_server/web_server.h @@ -42,13 +42,12 @@ using ParamNameType = const __FlashStringHelper *; using ParamNameType = const char *; #endif -// ESP8266 is single-threaded, so actions can execute directly in request context. -// Multi-core platforms need to defer to main loop thread for thread safety. -#ifdef USE_ESP8266 -#define DEFER_ACTION(capture, action) action -#else +// All platforms need to defer actions to main loop thread. +// Multi-core platforms need this for thread safety. +// ESP8266 needs this because ESPAsyncWebServer callbacks run in "sys" context +// (SDK system context), not "cont" context (continuation/main loop). Calling +// yield() from sys context causes a panic in the Arduino core. #define DEFER_ACTION(capture, action) this->defer([capture]() mutable { action; }) -#endif /// Result of matching a URL against an entity struct EntityMatchResult { diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index ae4bbfa557..f7b90018dc 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -74,7 +74,7 @@ void WebServer::set_css_url(const char *css_url) { this->css_url_ = css_url; } void WebServer::set_js_url(const char *js_url) { this->js_url_ = js_url; } void WebServer::handle_index_request(AsyncWebServerRequest *request) { - AsyncResponseStream *stream = request->beginResponseStream("text/html"); + AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("text/html")); const std::string &title = App.get_name(); stream->print(ESPHOME_F("")); diff --git a/esphome/components/web_server_base/__init__.py b/esphome/components/web_server_base/__init__.py index 6c756575d4..11408ae260 100644 --- a/esphome/components/web_server_base/__init__.py +++ b/esphome/components/web_server_base/__init__.py @@ -38,11 +38,8 @@ async def to_code(config): cg.add_define("WEB_SERVER_DEFAULT_HEADERS_COUNT", 1) return + # ESP32 uses IDF web server (early return above), so this is for other Arduino platforms if CORE.using_arduino: - if CORE.is_esp32: - cg.add_library("WiFi", None) - cg.add_library("FS", None) - cg.add_library("Update", None) if CORE.is_esp8266: cg.add_library("ESP8266WiFi", None) if CORE.is_libretiny: @@ -53,4 +50,4 @@ async def to_code(config): "lib_ignore", ["ESPAsyncTCP", "AsyncTCP", "AsyncTCP_RP2040W"] ) # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json - cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.5") + cg.add_library("ESP32Async/ESPAsyncWebServer", "3.9.6") diff --git a/esphome/components/web_server_idf/utils.cpp b/esphome/components/web_server_idf/utils.cpp index f27814062c..d3c3c3dc55 100644 --- a/esphome/components/web_server_idf/utils.cpp +++ b/esphome/components/web_server_idf/utils.cpp @@ -73,18 +73,15 @@ optional<std::string> request_get_url_query(httpd_req_t *req) { return {str}; } -optional<std::string> query_key_value(const std::string &query_url, const std::string &key) { - if (query_url.empty()) { +optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) { + if (query_url == nullptr || query_len == 0) { return {}; } - auto val = std::unique_ptr<char[]>(new char[query_url.size()]); - if (!val) { - ESP_LOGE(TAG, "Not enough memory to the query key value"); - return {}; - } + // Use stack buffer for typical query strings, heap fallback for large ones + SmallBufferWithHeapFallback<256, char> val(query_len); - if (httpd_query_key_value(query_url.c_str(), key.c_str(), val.get(), query_url.size()) != ESP_OK) { + if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) { return {}; } diff --git a/esphome/components/web_server_idf/utils.h b/esphome/components/web_server_idf/utils.h index 3a86aec7ac..ae58f82398 100644 --- a/esphome/components/web_server_idf/utils.h +++ b/esphome/components/web_server_idf/utils.h @@ -15,7 +15,10 @@ size_t url_decode(char *str); bool request_has_header(httpd_req_t *req, const char *name); optional<std::string> request_get_header(httpd_req_t *req, const char *name); optional<std::string> request_get_url_query(httpd_req_t *req); -optional<std::string> query_key_value(const std::string &query_url, const std::string &key); +optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key); +inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) { + return query_key_value(query_url.c_str(), query_url.size(), key.c_str()); +} // Helper function for case-insensitive character comparison inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); } diff --git a/esphome/components/web_server_idf/web_server_idf.cpp b/esphome/components/web_server_idf/web_server_idf.cpp index 2e5a74cbef..9860810452 100644 --- a/esphome/components/web_server_idf/web_server_idf.cpp +++ b/esphome/components/web_server_idf/web_server_idf.cpp @@ -366,7 +366,7 @@ void AsyncWebServerRequest::requestAuthentication(const char *realm) const { } #endif -AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { +AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) { // Check cache first - only successful lookups are cached for (auto *param : this->params_) { if (param->name() == name) { @@ -375,11 +375,11 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const std::string &name) { } // Look up value from query strings - optional<std::string> val = query_key_value(this->post_query_, name); + optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name); if (!val.has_value()) { auto url_query = request_get_url_query(*this); if (url_query.has_value()) { - val = query_key_value(url_query.value(), name); + val = query_key_value(url_query.value().c_str(), url_query.value().size(), name); } } diff --git a/esphome/components/web_server_idf/web_server_idf.h b/esphome/components/web_server_idf/web_server_idf.h index e38913ef4a..817f47da79 100644 --- a/esphome/components/web_server_idf/web_server_idf.h +++ b/esphome/components/web_server_idf/web_server_idf.h @@ -162,19 +162,24 @@ class AsyncWebServerRequest { } // NOLINTNEXTLINE(readability-identifier-naming) - bool hasParam(const std::string &name) { return this->getParam(name) != nullptr; } + bool hasParam(const char *name) { return this->getParam(name) != nullptr; } // NOLINTNEXTLINE(readability-identifier-naming) - AsyncWebParameter *getParam(const std::string &name); + bool hasParam(const std::string &name) { return this->getParam(name.c_str()) != nullptr; } + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const char *name); + // NOLINTNEXTLINE(readability-identifier-naming) + AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); } // NOLINTNEXTLINE(readability-identifier-naming) bool hasArg(const char *name) { return this->hasParam(name); } - std::string arg(const std::string &name) { + std::string arg(const char *name) { auto *param = this->getParam(name); if (param) { return param->value(); } return {}; } + std::string arg(const std::string &name) { return this->arg(name.c_str()); } operator httpd_req_t *() const { return this->req_; } optional<std::string> get_header(const char *name) const; diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 98266eb589..6863e6fb62 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -585,11 +585,13 @@ async def to_code(config): await cg.past_safe_mode() if on_connect_config := config.get(CONF_ON_CONNECT): + cg.add_define("USE_WIFI_CONNECT_TRIGGER") await automation.build_automation( var.get_connect_trigger(), [], on_connect_config ) if on_disconnect_config := config.get(CONF_ON_DISCONNECT): + cg.add_define("USE_WIFI_DISCONNECT_TRIGGER") await automation.build_automation( var.get_disconnect_trigger(), [], on_disconnect_config ) diff --git a/esphome/components/wifi/automation.h b/esphome/components/wifi/automation.h index fb0e71bcf6..1ad69b3992 100644 --- a/esphome/components/wifi/automation.h +++ b/esphome/components/wifi/automation.h @@ -48,7 +48,7 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), ssid.c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); return; } // Create a new WiFiAP object with the new SSID and password @@ -79,13 +79,13 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi // Start a timeout for the fallback if the connection to the old AP fails this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() { this->connecting_ = false; - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); }); }); } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; } - Trigger<> *get_error_trigger() const { return this->error_trigger_; } + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } + Trigger<> *get_error_trigger() { return &this->error_trigger_; } void loop() override { if (!this->connecting_) @@ -98,10 +98,10 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi char ssid_buf[SSID_BUFFER_SIZE]; if (strcmp(global_wifi_component->wifi_ssid_to(ssid_buf), this->new_sta_.get_ssid().c_str()) == 0) { // Callback to notify the user that the connection was successful - this->connect_trigger_->trigger(); + this->connect_trigger_.trigger(); } else { // Callback to notify the user that the connection failed - this->error_trigger_->trigger(); + this->error_trigger_.trigger(); } } } @@ -110,8 +110,8 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi bool connecting_{false}; WiFiAP new_sta_; WiFiAP old_sta_; - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *error_trigger_{new Trigger<>()}; + Trigger<> connect_trigger_; + Trigger<> error_trigger_; }; } // namespace esphome::wifi diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 65c653a62a..e350f990af 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -236,25 +236,23 @@ static const char *const TAG = "wifi"; /// │ - Roaming fail (RECONNECTING→IDLE): counter preserved (ping-pong) │ /// └──────────────────────────────────────────────────────────────────────┘ +// Use if-chain instead of switch to avoid jump table in RODATA (wastes RAM on ESP8266) static const LogString *retry_phase_to_log_string(WiFiRetryPhase phase) { - switch (phase) { - case WiFiRetryPhase::INITIAL_CONNECT: - return LOG_STR("INITIAL_CONNECT"); + if (phase == WiFiRetryPhase::INITIAL_CONNECT) + return LOG_STR("INITIAL_CONNECT"); #ifdef USE_WIFI_FAST_CONNECT - case WiFiRetryPhase::FAST_CONNECT_CYCLING_APS: - return LOG_STR("FAST_CONNECT_CYCLING"); + if (phase == WiFiRetryPhase::FAST_CONNECT_CYCLING_APS) + return LOG_STR("FAST_CONNECT_CYCLING"); #endif - case WiFiRetryPhase::EXPLICIT_HIDDEN: - return LOG_STR("EXPLICIT_HIDDEN"); - case WiFiRetryPhase::SCAN_CONNECTING: - return LOG_STR("SCAN_CONNECTING"); - case WiFiRetryPhase::RETRY_HIDDEN: - return LOG_STR("RETRY_HIDDEN"); - case WiFiRetryPhase::RESTARTING_ADAPTER: - return LOG_STR("RESTARTING"); - default: - return LOG_STR("UNKNOWN"); - } + if (phase == WiFiRetryPhase::EXPLICIT_HIDDEN) + return LOG_STR("EXPLICIT_HIDDEN"); + if (phase == WiFiRetryPhase::SCAN_CONNECTING) + return LOG_STR("SCAN_CONNECTING"); + if (phase == WiFiRetryPhase::RETRY_HIDDEN) + return LOG_STR("RETRY_HIDDEN"); + if (phase == WiFiRetryPhase::RESTARTING_ADAPTER) + return LOG_STR("RESTARTING"); + return LOG_STR("UNKNOWN"); } bool WiFiComponent::went_through_explicit_hidden_phase_() const { @@ -651,14 +649,21 @@ void WiFiComponent::loop() { const uint32_t now = App.get_loop_component_start_time(); if (this->has_sta()) { +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) if (this->is_connected() != this->handled_connected_state_) { +#ifdef USE_WIFI_DISCONNECT_TRIGGER if (this->handled_connected_state_) { - this->disconnect_trigger_->trigger(); - } else { - this->connect_trigger_->trigger(); + this->disconnect_trigger_.trigger(); } +#endif +#ifdef USE_WIFI_CONNECT_TRIGGER + if (!this->handled_connected_state_) { + this->connect_trigger_.trigger(); + } +#endif this->handled_connected_state_ = this->is_connected(); } +#endif // USE_WIFI_CONNECT_TRIGGER || USE_WIFI_DISCONNECT_TRIGGER switch (this->state_) { case WIFI_COMPONENT_STATE_COOLDOWN: { @@ -1457,6 +1462,20 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { this->release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Notify listeners now that state machine has reached STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->notify_connect_state_listeners_(); +#endif + +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) + // On ESP8266, GOT_IP event may not fire for static IP configurations, + // so notify IP state listeners here as a fallback. + if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { + this->notify_ip_state_listeners_(); + } +#endif + return; } @@ -1468,7 +1487,11 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { } if (this->error_from_callback_) { + // ESP8266: logging done in callback, listeners deferred via pending_.disconnect + // Other platforms: just log generic failure message +#ifndef USE_ESP8266 ESP_LOGW(TAG, "Connecting to network failed (callback)"); +#endif this->retry_connect(); return; } @@ -2176,6 +2199,44 @@ void WiFiComponent::release_scan_results_() { } } +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS +void WiFiComponent::notify_connect_state_listeners_() { + if (!this->pending_.connect_state) + return; + this->pending_.connect_state = false; + // Get current SSID and BSSID from the WiFi driver + char ssid_buf[SSID_BUFFER_SIZE]; + const char *ssid = this->wifi_ssid_to(ssid_buf); + bssid_t bssid = this->wifi_bssid(); + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(ssid, strlen(ssid)), bssid); + } +} + +void WiFiComponent::notify_disconnect_state_listeners_() { + constexpr uint8_t empty_bssid[6] = {}; + for (auto *listener : this->connect_state_listeners_) { + listener->on_wifi_connect_state(StringRef(), empty_bssid); + } +} +#endif // USE_WIFI_CONNECT_STATE_LISTENERS + +#ifdef USE_WIFI_IP_STATE_LISTENERS +void WiFiComponent::notify_ip_state_listeners_() { + for (auto *listener : this->ip_state_listeners_) { + listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); + } +} +#endif // USE_WIFI_IP_STATE_LISTENERS + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS +void WiFiComponent::notify_scan_results_listeners_() { + for (auto *listener : this->scan_results_listeners_) { + listener->on_wifi_scan_results(this->scan_result_); + } +} +#endif // USE_WIFI_SCAN_RESULTS_LISTENERS + void WiFiComponent::check_roaming_(uint32_t now) { // Guard: not for hidden networks (may not appear in scan) const WiFiAP *selected = this->get_selected_sta_(); diff --git a/esphome/components/wifi/wifi_component.h b/esphome/components/wifi/wifi_component.h index f27c522a1b..58f19c184a 100644 --- a/esphome/components/wifi/wifi_component.h +++ b/esphome/components/wifi/wifi_component.h @@ -454,8 +454,12 @@ class WiFiComponent : public Component { void set_keep_scan_results(bool keep_scan_results) { this->keep_scan_results_ = keep_scan_results; } void set_post_connect_roaming(bool enabled) { this->post_connect_roaming_ = enabled; } - Trigger<> *get_connect_trigger() const { return this->connect_trigger_; }; - Trigger<> *get_disconnect_trigger() const { return this->disconnect_trigger_; }; +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> *get_connect_trigger() { return &this->connect_trigger_; } +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; } +#endif int32_t get_wifi_channel(); @@ -590,6 +594,9 @@ class WiFiComponent : public Component { void connect_soon_(); void wifi_loop_(); +#ifdef USE_ESP8266 + void process_pending_callbacks_(); +#endif bool wifi_mode_(optional<bool> sta, optional<bool> ap); bool wifi_sta_pre_setup_(); bool wifi_apply_output_power_(float output_power); @@ -628,6 +635,21 @@ class WiFiComponent : public Component { /// Free scan results memory unless a component needs them void release_scan_results_(); +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + /// Notify connect state listeners (called after state machine reaches STA_CONNECTED) + void notify_connect_state_listeners_(); + /// Notify connect state listeners of disconnection + void notify_disconnect_state_listeners_(); +#endif +#ifdef USE_WIFI_IP_STATE_LISTENERS + /// Notify IP state listeners with current addresses + void notify_ip_state_listeners_(); +#endif +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + /// Notify scan results listeners with current scan results + void notify_scan_results_listeners_(); +#endif + #ifdef USE_ESP8266 static void wifi_event_callback(System_Event_t *event); void wifi_scan_done_callback_(void *arg, STATUS status); @@ -649,13 +671,13 @@ class WiFiComponent : public Component { void wifi_scan_done_callback_(); #endif + // Large/pointer-aligned members first FixedVector<WiFiAP> sta_; std::vector<WiFiSTAPriority> sta_priorities_; wifi_scan_vector_t<WiFiScanResult> scan_result_; #ifdef USE_WIFI_AP WiFiAP ap_; #endif - float output_power_{NAN}; #ifdef USE_WIFI_IP_STATE_LISTENERS StaticVector<WiFiIPStateListener *, ESPHOME_WIFI_IP_STATE_LISTENERS> ip_state_listeners_; #endif @@ -672,6 +694,15 @@ class WiFiComponent : public Component { #ifdef USE_WIFI_FAST_CONNECT ESPPreferenceObject fast_connect_pref_; #endif +#ifdef USE_WIFI_CONNECT_TRIGGER + Trigger<> connect_trigger_; +#endif +#ifdef USE_WIFI_DISCONNECT_TRIGGER + Trigger<> disconnect_trigger_; +#endif +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + SemaphoreHandle_t high_performance_semaphore_{nullptr}; +#endif // Post-connect roaming constants static constexpr uint32_t ROAMING_CHECK_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -679,7 +710,8 @@ class WiFiComponent : public Component { static constexpr int8_t ROAMING_GOOD_RSSI = -49; // Skip scan if signal is excellent static constexpr uint8_t ROAMING_MAX_ATTEMPTS = 3; - // Group all 32-bit integers together + // 4-byte members + float output_power_{NAN}; uint32_t action_started_; uint32_t last_connected_{0}; uint32_t reboot_timeout_{}; @@ -688,7 +720,7 @@ class WiFiComponent : public Component { uint32_t ap_timeout_{}; #endif - // Group all 8-bit values together + // 1-byte enums and integers WiFiComponentState state_{WIFI_COMPONENT_STATE_OFF}; WiFiPowerSaveMode power_save_{WIFI_POWER_SAVE_NONE}; WifiMinAuthMode min_auth_mode_{WIFI_MIN_AUTH_MODE_WPA2}; @@ -699,15 +731,39 @@ class WiFiComponent : public Component { // int8_t limits to 127 APs (enforced in __init__.py via MAX_WIFI_NETWORKS) int8_t selected_sta_index_{-1}; uint8_t roaming_attempts_{0}; - #if USE_NETWORK_IPV6 uint8_t num_ipv6_addresses_{0}; #endif /* USE_NETWORK_IPV6 */ - - // Group all boolean values together - bool has_ap_{false}; - bool handled_connected_state_{false}; bool error_from_callback_{false}; + RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; + RoamingState roaming_state_{RoamingState::IDLE}; +#if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) + WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; +#endif + + // Bools and bitfields + // Pending listener callbacks deferred from platform callbacks to main loop. + struct { +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + // Deferred until state machine reaches STA_CONNECTED so wifi.connected + // condition returns true in listener automations. + bool connect_state : 1; +#ifdef USE_ESP8266 + // ESP8266: also defer disconnect notification to main loop + bool disconnect : 1; +#endif +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_IP_STATE_LISTENERS) + bool got_ip : 1; +#endif +#if defined(USE_ESP8266) && defined(USE_WIFI_SCAN_RESULTS_LISTENERS) + bool scan_complete : 1; +#endif + } pending_{}; + bool has_ap_{false}; +#if defined(USE_WIFI_CONNECT_TRIGGER) || defined(USE_WIFI_DISCONNECT_TRIGGER) + bool handled_connected_state_{false}; +#endif bool scan_done_{false}; bool ap_setup_{false}; bool ap_started_{false}; @@ -722,21 +778,12 @@ class WiFiComponent : public Component { bool keep_scan_results_{false}; bool has_completed_scan_after_captive_portal_start_{ false}; // Tracks if we've completed a scan after captive portal started - RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY}; bool skip_cooldown_next_cycle_{false}; bool post_connect_roaming_{true}; // Enabled by default - RoamingState roaming_state_{RoamingState::IDLE}; #if defined(USE_ESP32) && defined(USE_WIFI_RUNTIME_POWER_SAVE) - WiFiPowerSaveMode configured_power_save_{WIFI_POWER_SAVE_NONE}; bool is_high_performance_mode_{false}; - - SemaphoreHandle_t high_performance_semaphore_{nullptr}; #endif - // Pointers at the end (naturally aligned) - Trigger<> *connect_trigger_{new Trigger<>()}; - Trigger<> *disconnect_trigger_{new Trigger<>()}; - private: // Stores a pointer to a string literal (static storage duration). // ONLY set from Python-generated code with string literals - never dynamic strings. diff --git a/esphome/components/wifi/wifi_component_esp8266.cpp b/esphome/components/wifi/wifi_component_esp8266.cpp index c714afaad3..6488de8dae 100644 --- a/esphome/components/wifi/wifi_component_esp8266.cpp +++ b/esphome/components/wifi/wifi_component_esp8266.cpp @@ -36,6 +36,7 @@ extern "C" { #include "esphome/core/hal.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include "esphome/core/util.h" namespace esphome::wifi { @@ -398,108 +399,88 @@ class WiFiMockClass : public ESP8266WiFiGenericClass { static void _event_callback(void *event) { ESP8266WiFiGenericClass::_eventCallback(event); } // NOLINT }; +// Auth mode strings indexed by AUTH_* constants (0-4), with UNKNOWN at last index +// Static asserts verify the SDK constants are contiguous as expected +static_assert(AUTH_OPEN == 0 && AUTH_WEP == 1 && AUTH_WPA_PSK == 2 && AUTH_WPA2_PSK == 3 && AUTH_WPA_WPA2_PSK == 4, + "AUTH_* constants are not contiguous"); +PROGMEM_STRING_TABLE(AuthModeStrings, "OPEN", "WEP", "WPA PSK", "WPA2 PSK", "WPA/WPA2 PSK", "UNKNOWN"); + const LogString *get_auth_mode_str(uint8_t mode) { - switch (mode) { - case AUTH_OPEN: - return LOG_STR("OPEN"); - case AUTH_WEP: - return LOG_STR("WEP"); - case AUTH_WPA_PSK: - return LOG_STR("WPA PSK"); - case AUTH_WPA2_PSK: - return LOG_STR("WPA2 PSK"); - case AUTH_WPA_WPA2_PSK: - return LOG_STR("WPA/WPA2 PSK"); - default: - return LOG_STR("UNKNOWN"); - } -} -const LogString *get_op_mode_str(uint8_t mode) { - switch (mode) { - case WIFI_OFF: - return LOG_STR("OFF"); - case WIFI_STA: - return LOG_STR("STA"); - case WIFI_AP: - return LOG_STR("AP"); - case WIFI_AP_STA: - return LOG_STR("AP+STA"); - default: - return LOG_STR("UNKNOWN"); - } + return AuthModeStrings::get_log_str(mode, AuthModeStrings::LAST_INDEX); } +// WiFi op mode strings indexed by WIFI_* constants (0-3), with UNKNOWN at last index +static_assert(WIFI_OFF == 0 && WIFI_STA == 1 && WIFI_AP == 2 && WIFI_AP_STA == 3, + "WIFI_* op mode constants are not contiguous"); +PROGMEM_STRING_TABLE(OpModeStrings, "OFF", "STA", "AP", "AP+STA", "UNKNOWN"); + +const LogString *get_op_mode_str(uint8_t mode) { return OpModeStrings::get_log_str(mode, OpModeStrings::LAST_INDEX); } + +// Use if-chain instead of switch to avoid jump tables in RODATA (wastes RAM on ESP8266). +// A single switch would generate a sparse lookup table with ~175 default entries, wasting 700 bytes of RAM. +// Even split switches still generate smaller jump tables in RODATA. const LogString *get_disconnect_reason_str(uint8_t reason) { - /* If this were one big switch statement, GCC would generate a lookup table for it. However, the values of the - * REASON_* constants aren't continuous, and GCC will fill in the gap with the default value -- wasting 4 bytes of RAM - * per entry. As there's ~175 default entries, this wastes 700 bytes of RAM. - */ - if (reason <= REASON_CIPHER_SUITE_REJECTED) { // This must be the last constant with a value <200 - switch (reason) { - case REASON_AUTH_EXPIRE: - return LOG_STR("Auth Expired"); - case REASON_AUTH_LEAVE: - return LOG_STR("Auth Leave"); - case REASON_ASSOC_EXPIRE: - return LOG_STR("Association Expired"); - case REASON_ASSOC_TOOMANY: - return LOG_STR("Too Many Associations"); - case REASON_NOT_AUTHED: - return LOG_STR("Not Authenticated"); - case REASON_NOT_ASSOCED: - return LOG_STR("Not Associated"); - case REASON_ASSOC_LEAVE: - return LOG_STR("Association Leave"); - case REASON_ASSOC_NOT_AUTHED: - return LOG_STR("Association not Authenticated"); - case REASON_DISASSOC_PWRCAP_BAD: - return LOG_STR("Disassociate Power Cap Bad"); - case REASON_DISASSOC_SUPCHAN_BAD: - return LOG_STR("Disassociate Supported Channel Bad"); - case REASON_IE_INVALID: - return LOG_STR("IE Invalid"); - case REASON_MIC_FAILURE: - return LOG_STR("Mic Failure"); - case REASON_4WAY_HANDSHAKE_TIMEOUT: - return LOG_STR("4-Way Handshake Timeout"); - case REASON_GROUP_KEY_UPDATE_TIMEOUT: - return LOG_STR("Group Key Update Timeout"); - case REASON_IE_IN_4WAY_DIFFERS: - return LOG_STR("IE In 4-Way Handshake Differs"); - case REASON_GROUP_CIPHER_INVALID: - return LOG_STR("Group Cipher Invalid"); - case REASON_PAIRWISE_CIPHER_INVALID: - return LOG_STR("Pairwise Cipher Invalid"); - case REASON_AKMP_INVALID: - return LOG_STR("AKMP Invalid"); - case REASON_UNSUPP_RSN_IE_VERSION: - return LOG_STR("Unsupported RSN IE version"); - case REASON_INVALID_RSN_IE_CAP: - return LOG_STR("Invalid RSN IE Cap"); - case REASON_802_1X_AUTH_FAILED: - return LOG_STR("802.1x Authentication Failed"); - case REASON_CIPHER_SUITE_REJECTED: - return LOG_STR("Cipher Suite Rejected"); - } - } - - switch (reason) { - case REASON_BEACON_TIMEOUT: - return LOG_STR("Beacon Timeout"); - case REASON_NO_AP_FOUND: - return LOG_STR("AP Not Found"); - case REASON_AUTH_FAIL: - return LOG_STR("Authentication Failed"); - case REASON_ASSOC_FAIL: - return LOG_STR("Association Failed"); - case REASON_HANDSHAKE_TIMEOUT: - return LOG_STR("Handshake Failed"); - case REASON_UNSPECIFIED: - default: - return LOG_STR("Unspecified"); - } + if (reason == REASON_AUTH_EXPIRE) + return LOG_STR("Auth Expired"); + if (reason == REASON_AUTH_LEAVE) + return LOG_STR("Auth Leave"); + if (reason == REASON_ASSOC_EXPIRE) + return LOG_STR("Association Expired"); + if (reason == REASON_ASSOC_TOOMANY) + return LOG_STR("Too Many Associations"); + if (reason == REASON_NOT_AUTHED) + return LOG_STR("Not Authenticated"); + if (reason == REASON_NOT_ASSOCED) + return LOG_STR("Not Associated"); + if (reason == REASON_ASSOC_LEAVE) + return LOG_STR("Association Leave"); + if (reason == REASON_ASSOC_NOT_AUTHED) + return LOG_STR("Association not Authenticated"); + if (reason == REASON_DISASSOC_PWRCAP_BAD) + return LOG_STR("Disassociate Power Cap Bad"); + if (reason == REASON_DISASSOC_SUPCHAN_BAD) + return LOG_STR("Disassociate Supported Channel Bad"); + if (reason == REASON_IE_INVALID) + return LOG_STR("IE Invalid"); + if (reason == REASON_MIC_FAILURE) + return LOG_STR("Mic Failure"); + if (reason == REASON_4WAY_HANDSHAKE_TIMEOUT) + return LOG_STR("4-Way Handshake Timeout"); + if (reason == REASON_GROUP_KEY_UPDATE_TIMEOUT) + return LOG_STR("Group Key Update Timeout"); + if (reason == REASON_IE_IN_4WAY_DIFFERS) + return LOG_STR("IE In 4-Way Handshake Differs"); + if (reason == REASON_GROUP_CIPHER_INVALID) + return LOG_STR("Group Cipher Invalid"); + if (reason == REASON_PAIRWISE_CIPHER_INVALID) + return LOG_STR("Pairwise Cipher Invalid"); + if (reason == REASON_AKMP_INVALID) + return LOG_STR("AKMP Invalid"); + if (reason == REASON_UNSUPP_RSN_IE_VERSION) + return LOG_STR("Unsupported RSN IE version"); + if (reason == REASON_INVALID_RSN_IE_CAP) + return LOG_STR("Invalid RSN IE Cap"); + if (reason == REASON_802_1X_AUTH_FAILED) + return LOG_STR("802.1x Authentication Failed"); + if (reason == REASON_CIPHER_SUITE_REJECTED) + return LOG_STR("Cipher Suite Rejected"); + if (reason == REASON_BEACON_TIMEOUT) + return LOG_STR("Beacon Timeout"); + if (reason == REASON_NO_AP_FOUND) + return LOG_STR("AP Not Found"); + if (reason == REASON_AUTH_FAIL) + return LOG_STR("Authentication Failed"); + if (reason == REASON_ASSOC_FAIL) + return LOG_STR("Association Failed"); + if (reason == REASON_HANDSHAKE_TIMEOUT) + return LOG_STR("Handshake Failed"); + return LOG_STR("Unspecified"); } +// TODO: This callback runs in ESP8266 system context with limited stack (~2KB). +// All listener notifications should be deferred to wifi_loop_() via pending_ flags +// to avoid stack overflow. Currently only connect_state is deferred; disconnect, +// IP, and scan listeners still run in this context and should be migrated. void WiFiComponent::wifi_event_callback(System_Event_t *event) { switch (event->event) { case EVENT_STAMODE_CONNECTED: { @@ -512,19 +493,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } -#endif - // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here -#if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) - if (const WiFiAP *config = global_wifi_component->get_selected_sta_(); - config && config->get_manual_ip().has_value()) { - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), - global_wifi_component->get_dns_address(0), global_wifi_component->get_dns_address(1)); - } - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + global_wifi_component->pending_.connect_state = true; #endif break; } @@ -543,16 +514,9 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { } s_sta_connected = false; s_sta_connecting = false; - // IMPORTANT: Set error flag BEFORE notifying listeners. - // This ensures is_connected() returns false during listener callbacks, - // which is critical for proper reconnection logic (e.g., roaming). global_wifi_component->error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - // Notify listeners AFTER setting error flag so they see correct state - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : global_wifi_component->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + global_wifi_component->pending_.disconnect = true; #endif break; } @@ -564,8 +528,6 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { // https://lbsfilm.at/blog/wpa2-authenticationmode-downgrade-in-espressif-microprocessors if (it.old_mode != AUTH_OPEN && it.new_mode == AUTH_OPEN) { ESP_LOGW(TAG, "Potential Authmode downgrade detected, disconnecting"); - // we can't call retry_connect() from this context, so disconnect immediately - // and notify main thread with error_from_callback_ wifi_station_disconnect(); global_wifi_component->error_from_callback_ = true; } @@ -579,10 +541,8 @@ void WiFiComponent::wifi_event_callback(System_Event_t *event) { network::IPAddress(&it.gw).str_to(gw_buf), network::IPAddress(&it.mask).str_to(mask_buf)); s_sta_got_ip = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : global_wifi_component->ip_state_listeners_) { - listener->on_ip_state(global_wifi_component->wifi_sta_ip_addresses(), global_wifi_component->get_dns_address(0), - global_wifi_component->get_dns_address(1)); - } + // Defer listener callbacks to main loop - system context has limited stack + global_wifi_component->pending_.got_ip = true; #endif break; } @@ -675,21 +635,15 @@ void WiFiComponent::wifi_pre_setup_() { WiFiSTAConnectStatus WiFiComponent::wifi_sta_connect_status_() { station_status_t status = wifi_station_get_connect_status(); - switch (status) { - case STATION_GOT_IP: - return WiFiSTAConnectStatus::CONNECTED; - case STATION_NO_AP_FOUND: - return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; - ; - case STATION_CONNECT_FAIL: - case STATION_WRONG_PASSWORD: - return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; - case STATION_CONNECTING: - return WiFiSTAConnectStatus::CONNECTING; - case STATION_IDLE: - default: - return WiFiSTAConnectStatus::IDLE; - } + if (status == STATION_GOT_IP) + return WiFiSTAConnectStatus::CONNECTED; + if (status == STATION_NO_AP_FOUND) + return WiFiSTAConnectStatus::ERROR_NETWORK_NOT_FOUND; + if (status == STATION_CONNECT_FAIL || status == STATION_WRONG_PASSWORD) + return WiFiSTAConnectStatus::ERROR_CONNECT_FAILED; + if (status == STATION_CONNECTING) + return WiFiSTAConnectStatus::CONNECTING; + return WiFiSTAConnectStatus::IDLE; } bool WiFiComponent::wifi_scan_start_(bool passive) { static bool first_scan = false; @@ -794,9 +748,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) { needs_full ? "" : " (filtered)"); this->scan_done_ = true; #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : global_wifi_component->scan_results_listeners_) { - listener->on_wifi_scan_results(global_wifi_component->scan_result_); - } + this->pending_.scan_complete = true; // Defer listener callbacks to main loop #endif } @@ -983,7 +935,34 @@ network::IPAddress WiFiComponent::wifi_gateway_ip_() { return network::IPAddress(&ip.gw); } network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); } -void WiFiComponent::wifi_loop_() {} +void WiFiComponent::wifi_loop_() { this->process_pending_callbacks_(); } + +void WiFiComponent::process_pending_callbacks_() { + // Process callbacks deferred from ESP8266 SDK system context (~2KB stack) + // to main loop context (full stack). Connect state listeners are handled + // by notify_connect_state_listeners_() in the shared state machine code. + +#ifdef USE_WIFI_CONNECT_STATE_LISTENERS + if (this->pending_.disconnect) { + this->pending_.disconnect = false; + this->notify_disconnect_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_IP_STATE_LISTENERS + if (this->pending_.got_ip) { + this->pending_.got_ip = false; + this->notify_ip_state_listeners_(); + } +#endif + +#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS + if (this->pending_.scan_complete) { + this->pending_.scan_complete = false; + this->notify_scan_results_listeners_(); + } +#endif +} } // namespace esphome::wifi #endif diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index a32232a758..d74d083954 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -710,6 +710,9 @@ void WiFiComponent::wifi_loop_() { delete data; // NOLINT(cppcoreguidelines-owning-memory) } } +// Events are processed from queue in main loop context, but listener notifications +// must be deferred until after the state machine transitions (in check_connecting_finished) +// so that conditions like wifi.connected return correct values in automations. void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { esp_err_t err; if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_STA_START) { @@ -743,16 +746,14 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { #endif s_sta_connected = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so notify IP listeners here #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif @@ -776,10 +777,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { s_sta_connecting = false; error_from_callback_ = true; #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } else if (data->event_base == IP_EVENT && data->event_id == IP_EVENT_STA_GOT_IP) { @@ -790,9 +788,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "static_ip=" IPSTR " gateway=" IPSTR, IP2STR(&it.ip_info.ip), IP2STR(&it.ip_info.gw)); this->got_ipv4_address_ = true; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #if USE_NETWORK_IPV6 @@ -801,9 +797,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "IPv6 address=" IPV6STR, IPV62STR(it.ip6_info.ip)); this->num_ipv6_addresses_++; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif #endif /* USE_NETWORK_IPV6 */ @@ -880,9 +874,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) { ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } else if (data->event_base == WIFI_EVENT && data->event_id == WIFI_EVENT_AP_START) { diff --git a/esphome/components/wifi/wifi_component_libretiny.cpp b/esphome/components/wifi/wifi_component_libretiny.cpp index af2b82c3c6..5fd9d7663b 100644 --- a/esphome/components/wifi/wifi_component_libretiny.cpp +++ b/esphome/components/wifi/wifi_component_libretiny.cpp @@ -423,7 +423,10 @@ void WiFiComponent::wifi_event_callback_(esphome_wifi_event_id_t event, esphome_ } } -// Process a single event from the queue - runs in main loop context +// Process a single event from the queue - runs in main loop context. +// Listener notifications must be deferred until after the state machine transitions +// (in check_connecting_finished) so that conditions like wifi.connected return +// correct values in automations. void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { switch (event->event_id) { case ESPHOME_EVENT_ID_WIFI_READY: { @@ -456,18 +459,16 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { // This matches ESP32 IDF behavior where s_sta_connected is set but // wifi_sta_connect_status_() also checks got_ipv4_address_ #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(it.ssid, it.ssid_len), it.bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, GOT_IP event may not fire, so set connected state here #ifdef USE_WIFI_MANUAL_IP if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } #endif @@ -524,10 +525,7 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { } #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif break; } @@ -550,18 +548,14 @@ void WiFiComponent::wifi_process_event_(LTWiFiEvent *event) { network::IPAddress(WiFi.gatewayIP()).str_to(gw_buf)); s_sta_state = LTWiFiSTAState::CONNECTED; #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } case ESPHOME_EVENT_ID_WIFI_STA_GOT_IP6: { ESP_LOGV(TAG, "Got IPv6"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif break; } @@ -705,9 +699,7 @@ void WiFiComponent::wifi_scan_done_callback_() { needs_full ? "" : " (filtered)"); WiFi.scanDelete(); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } diff --git a/esphome/components/wifi/wifi_component_pico_w.cpp b/esphome/components/wifi/wifi_component_pico_w.cpp index 84c10d5d43..818ad1059c 100644 --- a/esphome/components/wifi/wifi_component_pico_w.cpp +++ b/esphome/components/wifi/wifi_component_pico_w.cpp @@ -252,6 +252,10 @@ network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_ip); } +// Pico W uses polling for connection state detection. +// Connect state listener notifications are deferred until after the state machine +// transitions (in check_connecting_finished) so that conditions like wifi.connected +// return correct values in automations. void WiFiComponent::wifi_loop_() { // Handle scan completion if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) { @@ -260,9 +264,7 @@ void WiFiComponent::wifi_loop_() { ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(), needs_full ? "" : " (filtered)"); #ifdef USE_WIFI_SCAN_RESULTS_LISTENERS - for (auto *listener : this->scan_results_listeners_) { - listener->on_wifi_scan_results(this->scan_result_); - } + this->notify_scan_results_listeners_(); #endif } @@ -278,19 +280,15 @@ void WiFiComponent::wifi_loop_() { s_sta_was_connected = true; ESP_LOGV(TAG, "Connected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - String ssid = WiFi.SSID(); - bssid_t bssid = this->wifi_bssid(); - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(ssid.c_str(), ssid.length()), bssid); - } + // Defer listener notification until state machine reaches STA_CONNECTED + // This ensures wifi.connected condition returns true in listener automations + this->pending_.connect_state = true; #endif // For static IP configurations, notify IP listeners immediately as the IP is already configured #if defined(USE_WIFI_IP_STATE_LISTENERS) && defined(USE_WIFI_MANUAL_IP) if (const WiFiAP *config = this->get_selected_sta_(); config && config->get_manual_ip().has_value()) { s_sta_had_ip = true; - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); } #endif } else if (!is_connected && s_sta_was_connected) { @@ -299,10 +297,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = false; ESP_LOGV(TAG, "Disconnected"); #ifdef USE_WIFI_CONNECT_STATE_LISTENERS - static constexpr uint8_t EMPTY_BSSID[6] = {}; - for (auto *listener : this->connect_state_listeners_) { - listener->on_wifi_connect_state(StringRef(), EMPTY_BSSID); - } + this->notify_disconnect_state_listeners_(); #endif } @@ -320,9 +315,7 @@ void WiFiComponent::wifi_loop_() { s_sta_had_ip = true; ESP_LOGV(TAG, "Got IP address"); #ifdef USE_WIFI_IP_STATE_LISTENERS - for (auto *listener : this->ip_state_listeners_) { - listener->on_ip_state(this->wifi_sta_ip_addresses(), this->get_dns_address(0), this->get_dns_address(1)); - } + this->notify_ip_state_listeners_(); #endif } } diff --git a/esphome/components/wled/__init__.py b/esphome/components/wled/__init__.py index fb20a03010..49eb15dad6 100644 --- a/esphome/components/wled/__init__.py +++ b/esphome/components/wled/__init__.py @@ -3,6 +3,7 @@ from esphome.components.light.effects import register_addressable_effect from esphome.components.light.types import AddressableLightEffect import esphome.config_validation as cv from esphome.const import CONF_NAME, CONF_PORT +from esphome.core import CORE wled_ns = cg.esphome_ns.namespace("wled") WLEDLightEffect = wled_ns.class_("WLEDLightEffect", AddressableLightEffect) @@ -27,4 +28,6 @@ async def wled_light_effect_to_code(config, effect_id): cg.add(effect.set_port(config[CONF_PORT])) cg.add(effect.set_sync_group_mask(config[CONF_SYNC_GROUP_MASK])) cg.add(effect.set_blank_on_start(config[CONF_BLANK_ON_START])) + if CORE.is_esp32: + cg.add_library("WiFi", None) return effect diff --git a/esphome/components/wts01/wts01.h b/esphome/components/wts01/wts01.h index 298595a5d6..aae90c2c77 100644 --- a/esphome/components/wts01/wts01.h +++ b/esphome/components/wts01/wts01.h @@ -13,7 +13,6 @@ class WTS01Sensor : public sensor::Sensor, public uart::UARTDevice, public Compo public: void loop() override; void dump_config() override; - float get_setup_priority() const override { return setup_priority::DATA; } protected: uint8_t buffer_[PACKET_SIZE]; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 8e3ae86bbe..43d5cebebb 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -213,9 +213,10 @@ def copy_files(): zephyr_data()[KEY_OVERLAY], ) - if zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT or zephyr_data()[ - KEY_BOARD - ] in ["xiao_ble"]: + if ( + zephyr_data()[KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT + or zephyr_data()[KEY_BOARD] == "xiao_ble" + ): fake_board_manifest = """ { "frameworks": [ diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index 311133a813..f02fa16326 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -152,10 +152,11 @@ class ZephyrPreferences : public ESPPreferences { } }; +static ZephyrPreferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + void setup_preferences() { - auto *prefs = new ZephyrPreferences(); // NOLINT(cppcoreguidelines-owning-memory) - global_preferences = prefs; - prefs->open(); + global_preferences = &s_preferences; + s_preferences.open(); } } // namespace zephyr diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index 1b1da78308..7e917a9d70 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -24,7 +24,12 @@ from .const_zephyr import ( ZigbeeComponent, zigbee_ns, ) -from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor, zephyr_switch +from .zigbee_zephyr import ( + zephyr_binary_sensor, + zephyr_number, + zephyr_sensor, + zephyr_switch, +) _LOGGER = logging.getLogger(__name__) @@ -43,6 +48,7 @@ def zigbee_set_core_data(config: ConfigType) -> ConfigType: BINARY_SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_binary_sensor) SENSOR_SCHEMA = cv.Schema({}).extend(zephyr_sensor) SWITCH_SCHEMA = cv.Schema({}).extend(zephyr_switch) +NUMBER_SCHEMA = cv.Schema({}).extend(zephyr_number) CONFIG_SCHEMA = cv.All( cv.Schema( @@ -78,8 +84,8 @@ def validate_number_of_ep(config: ConfigType) -> None: raise cv.Invalid("At least one zigbee device need to be included") count = len(CORE.data[KEY_ZIGBEE][KEY_EP_NUMBER]) if count == 1: - raise cv.Invalid( - "Single endpoint is not supported https://github.com/Koenkk/zigbee2mqtt/issues/29888" + _LOGGER.warning( + "Single endpoint requires ZHA or at leatst Zigbee2MQTT 2.8.0. For older versions of Zigbee2MQTT use multiple endpoints" ) if count > CONF_MAX_EP_NUMBER and not CORE.testing_mode: raise cv.Invalid(f"Maximum number of end points is {CONF_MAX_EP_NUMBER}") @@ -125,12 +131,27 @@ async def setup_switch(entity: cg.MockObj, config: ConfigType) -> None: await zephyr_setup_switch(entity, config) +async def setup_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): + return + if CORE.using_zephyr: + from .zigbee_zephyr import zephyr_setup_number + + await zephyr_setup_number(entity, config, min_value, max_value, step) + + def consume_endpoint(config: ConfigType) -> ConfigType: if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): return config if CONF_NAME in config and " " in config[CONF_NAME]: _LOGGER.warning( - "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", + "Spaces in '%s' requires ZHA or at least Zigbee2MQTT 2.8.0. For older version of Zigbee2MQTT use '%s'", config[CONF_NAME], config[CONF_NAME].replace(" ", "_"), ) @@ -152,6 +173,10 @@ def validate_switch(config: ConfigType) -> ConfigType: return consume_endpoint(config) +def validate_number(config: ConfigType) -> ConfigType: + return consume_endpoint(config) + + ZIGBEE_ACTION_SCHEMA = automation.maybe_simple_id( cv.Schema( { diff --git a/esphome/components/zigbee/const_zephyr.py b/esphome/components/zigbee/const_zephyr.py index 03c1bb546f..2d233755ac 100644 --- a/esphome/components/zigbee/const_zephyr.py +++ b/esphome/components/zigbee/const_zephyr.py @@ -4,6 +4,7 @@ zigbee_ns = cg.esphome_ns.namespace("zigbee") ZigbeeComponent = zigbee_ns.class_("ZigbeeComponent", cg.Component) BinaryAttrs = zigbee_ns.struct("BinaryAttrs") AnalogAttrs = zigbee_ns.struct("AnalogAttrs") +AnalogAttrsOutput = zigbee_ns.struct("AnalogAttrsOutput") CONF_MAX_EP_NUMBER = 8 CONF_ZIGBEE_ID = "zigbee_id" @@ -12,6 +13,7 @@ CONF_WIPE_ON_BOOT = "wipe_on_boot" CONF_ZIGBEE_BINARY_SENSOR = "zigbee_binary_sensor" CONF_ZIGBEE_SENSOR = "zigbee_sensor" CONF_ZIGBEE_SWITCH = "zigbee_switch" +CONF_ZIGBEE_NUMBER = "zigbee_number" CONF_POWER_SOURCE = "power_source" POWER_SOURCE = { "UNKNOWN": "ZB_ZCL_BASIC_POWER_SOURCE_UNKNOWN", @@ -38,3 +40,4 @@ ZB_ZCL_CLUSTER_ID_IDENTIFY = "ZB_ZCL_CLUSTER_ID_IDENTIFY" ZB_ZCL_CLUSTER_ID_BINARY_INPUT = "ZB_ZCL_CLUSTER_ID_BINARY_INPUT" ZB_ZCL_CLUSTER_ID_ANALOG_INPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_INPUT" ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT = "ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT" +ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT = "ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT" diff --git a/esphome/components/zigbee/zigbee_number_zephyr.cpp b/esphome/components/zigbee/zigbee_number_zephyr.cpp new file mode 100644 index 0000000000..ceb318480c --- /dev/null +++ b/esphome/components/zigbee/zigbee_number_zephyr.cpp @@ -0,0 +1,111 @@ +#include "zigbee_number_zephyr.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER) +#include "esphome/core/log.h" +extern "C" { +#include <zboss_api.h> +#include <zboss_api_addons.h> +#include <zb_nrf_platform.h> +#include <zigbee/zigbee_app_utils.h> +#include <zb_error_to_string.h> +} +namespace esphome::zigbee { + +static const char *const TAG = "zigbee.number"; + +void ZigbeeNumber::setup() { + this->parent_->add_callback(this->endpoint_, [this](zb_bufid_t bufid) { this->zcl_device_cb_(bufid); }); + this->number_->add_on_state_callback([this](float state) { + this->cluster_attributes_->present_value = state; + ESP_LOGD(TAG, "Set attribute endpoint: %d, present_value %f", this->endpoint_, + this->cluster_attributes_->present_value); + ZB_ZCL_SET_ATTRIBUTE(this->endpoint_, ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (zb_uint8_t *) &cluster_attributes_->present_value, + ZB_FALSE); + this->parent_->force_report(); + }); +} + +void ZigbeeNumber::dump_config() { + ESP_LOGCONFIG(TAG, + "Zigbee Number\n" + " Endpoint: %d, present_value %f", + this->endpoint_, this->cluster_attributes_->present_value); +} + +void ZigbeeNumber::zcl_device_cb_(zb_bufid_t bufid) { + zb_zcl_device_callback_param_t *p_device_cb_param = ZB_BUF_GET_PARAM(bufid, zb_zcl_device_callback_param_t); + zb_zcl_device_callback_id_t device_cb_id = p_device_cb_param->device_cb_id; + zb_uint16_t cluster_id = p_device_cb_param->cb_param.set_attr_value_param.cluster_id; + zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; + + switch (device_cb_id) { + /* ZCL set attribute value */ + case ZB_ZCL_SET_ATTR_VALUE_CB_ID: + if (cluster_id == ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT) { + ESP_LOGI(TAG, "Analog output attribute setting"); + if (attr_id == ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID) { + float value = + *reinterpret_cast<const float *>(&p_device_cb_param->cb_param.set_attr_value_param.values.data32); + this->defer([this, value]() { + this->cluster_attributes_->present_value = value; + auto call = this->number_->make_call(); + call.set_value(value); + call.perform(); + }); + } + } else { + /* other clusters attribute handled here */ + ESP_LOGI(TAG, "Unhandled cluster attribute id: %d", cluster_id); + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + } + break; + default: + p_device_cb_param->status = RET_NOT_IMPLEMENTED; + break; + } + + ESP_LOGD(TAG, "%s status: %hd", __func__, p_device_cb_param->status); +} + +const zb_uint8_t ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE = 0x0F; + +static zb_ret_t check_value_analog_server(zb_uint16_t attr_id, zb_uint8_t endpoint, + zb_uint8_t *value) { // NOLINT(readability-non-const-parameter) + zb_ret_t ret = RET_OK; + ZVUNUSED(endpoint); + + switch (attr_id) { + case ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID: + ret = ZB_ZCL_CHECK_BOOL_VALUE(*value) ? RET_OK : RET_ERROR; + break; + case ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID: + break; + + case ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID: + if (*value > ZB_ZCL_ANALOG_OUTPUT_STATUS_FLAG_MAX_VALUE) { + ret = RET_ERROR; + } + break; + + default: + break; + } + + return ret; +} + +} // namespace esphome::zigbee + +void zb_zcl_analog_output_init_server() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_SERVER_ROLE, + esphome::zigbee::check_value_analog_server, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +void zb_zcl_analog_output_init_client() { + zb_zcl_add_cluster_handlers(ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_CLIENT_ROLE, + (zb_zcl_cluster_check_value_t) NULL, (zb_zcl_cluster_write_attr_hook_t) NULL, + (zb_zcl_cluster_handler_t) NULL); +} + +#endif diff --git a/esphome/components/zigbee/zigbee_number_zephyr.h b/esphome/components/zigbee/zigbee_number_zephyr.h new file mode 100644 index 0000000000..aabb0392be --- /dev/null +++ b/esphome/components/zigbee/zigbee_number_zephyr.h @@ -0,0 +1,118 @@ +#pragma once + +#include "esphome/core/defines.h" +#if defined(USE_ZIGBEE) && defined(USE_NRF52) && defined(USE_NUMBER) +#include "esphome/components/zigbee/zigbee_zephyr.h" +#include "esphome/core/component.h" +#include "esphome/components/number/number.h" +extern "C" { +#include <zboss_api.h> +#include <zboss_api_addons.h> +} + +enum { + ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID = 0x001C, + ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID = 0x0041, + ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID = 0x0045, + ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID = 0x0051, + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID = 0x0055, + ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID = 0x006A, + ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID = 0x006F, + ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID = 0x0075, +}; + +#define ZB_ZCL_ANALOG_OUTPUT_CLUSTER_REVISION_DEFAULT ((zb_uint16_t) 0x0001u) + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, ZB_ZCL_ATTR_TYPE_CHAR_STRING, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, ZB_ZCL_ATTR_TYPE_BOOL, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// PresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_WRITE | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// MaxPresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// MinPresentValue +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } +// Resolution +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, ZB_ZCL_ATTR_TYPE_SINGLE, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_WRITE_OPTIONAL, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, ZB_ZCL_ATTR_TYPE_8BITMAP, \ + ZB_ZCL_ATTR_ACCESS_READ_ONLY | ZB_ZCL_ATTR_ACCESS_REPORTING, (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), \ + (void *) (data_ptr) \ + } + +#define ZB_SET_ATTR_DESCR_WITH_ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID(data_ptr) \ + { \ + ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, ZB_ZCL_ATTR_TYPE_16BIT_ENUM, ZB_ZCL_ATTR_ACCESS_READ_ONLY, \ + (ZB_ZCL_NON_MANUFACTURER_SPECIFIC), (void *) (data_ptr) \ + } + +#define ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST(attr_list, out_of_service, present_value, status_flag, \ + max_present_value, min_present_value, resolution, \ + engineering_units, description) \ + ZB_ZCL_START_DECLARE_ATTRIB_LIST_CLUSTER_REVISION(attr_list, ZB_ZCL_ANALOG_OUTPUT) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_OUT_OF_SERVICE_ID, (out_of_service)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_PRESENT_VALUE_ID, (present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_STATUS_FLAG_ID, (status_flag)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MAX_PRESENT_VALUE_ID, (max_present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_MIN_PRESENT_VALUE_ID, (min_present_value)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_RESOLUTION_ID, (resolution)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_ENGINEERING_UNITS_ID, (engineering_units)) \ + ZB_ZCL_SET_ATTR_DESC(ZB_ZCL_ATTR_ANALOG_OUTPUT_DESCRIPTION_ID, (description)) \ + ZB_ZCL_FINISH_DECLARE_ATTRIB_LIST + +void zb_zcl_analog_output_init_server(); +void zb_zcl_analog_output_init_client(); +#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_SERVER_ROLE_INIT zb_zcl_analog_output_init_server +#define ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT_CLIENT_ROLE_INIT zb_zcl_analog_output_init_client + +namespace esphome::zigbee { + +class ZigbeeNumber : public ZigbeeEntity, public Component { + public: + ZigbeeNumber(number::Number *n) : number_(n) {} + void set_cluster_attributes(AnalogAttrsOutput &cluster_attributes) { + this->cluster_attributes_ = &cluster_attributes; + } + + void setup() override; + void dump_config() override; + + protected: + number::Number *number_; + AnalogAttrsOutput *cluster_attributes_{nullptr}; + void zcl_device_cb_(zb_bufid_t bufid); +}; + +} // namespace esphome::zigbee +#endif diff --git a/esphome/components/zigbee/zigbee_switch_zephyr.cpp b/esphome/components/zigbee/zigbee_switch_zephyr.cpp index fef02e5a0c..935140e9df 100644 --- a/esphome/components/zigbee/zigbee_switch_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_switch_zephyr.cpp @@ -50,7 +50,7 @@ void ZigbeeSwitch::zcl_device_cb_(zb_bufid_t bufid) { if (attr_id == ZB_ZCL_ATTR_BINARY_OUTPUT_PRESENT_VALUE_ID) { this->defer([this, value]() { this->cluster_attributes_->present_value = value ? ZB_TRUE : ZB_FALSE; - this->switch_->publish_state(value); + this->switch_->control(value); }); } } else { diff --git a/esphome/components/zigbee/zigbee_zephyr.cpp b/esphome/components/zigbee/zigbee_zephyr.cpp index eabf5c30d4..4763943e88 100644 --- a/esphome/components/zigbee/zigbee_zephyr.cpp +++ b/esphome/components/zigbee/zigbee_zephyr.cpp @@ -101,8 +101,8 @@ void ZigbeeComponent::zcl_device_cb(zb_bufid_t bufid) { zb_uint16_t attr_id = p_device_cb_param->cb_param.set_attr_value_param.attr_id; auto endpoint = p_device_cb_param->endpoint; - ESP_LOGI(TAG, "Zcl_device_cb %s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, - attr_id, endpoint); + ESP_LOGI(TAG, "%s id %hd, cluster_id %d, attr_id %d, endpoint: %d", __func__, device_cb_id, cluster_id, attr_id, + endpoint); /* Set default response value. */ p_device_cb_param->status = RET_OK; diff --git a/esphome/components/zigbee/zigbee_zephyr.h b/esphome/components/zigbee/zigbee_zephyr.h index b75c192c1a..05895e8e61 100644 --- a/esphome/components/zigbee/zigbee_zephyr.h +++ b/esphome/components/zigbee/zigbee_zephyr.h @@ -60,6 +60,12 @@ struct AnalogAttrs { zb_uchar_t description[ZB_ZCL_MAX_STRING_SIZE]; }; +struct AnalogAttrsOutput : AnalogAttrs { + float max_present_value; + float min_present_value; + float resolution; +}; + class ZigbeeComponent : public Component { public: void setup() override; diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index b3bd10bfab..0b6daa9476 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -55,6 +55,7 @@ from .const_zephyr import ( CONF_WIPE_ON_BOOT, CONF_ZIGBEE_BINARY_SENSOR, CONF_ZIGBEE_ID, + CONF_ZIGBEE_NUMBER, CONF_ZIGBEE_SENSOR, CONF_ZIGBEE_SWITCH, KEY_EP_NUMBER, @@ -62,12 +63,14 @@ from .const_zephyr import ( POWER_SOURCE, ZB_ZCL_BASIC_ATTRS_EXT_T, ZB_ZCL_CLUSTER_ID_ANALOG_INPUT, + ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, ZB_ZCL_CLUSTER_ID_BASIC, ZB_ZCL_CLUSTER_ID_BINARY_INPUT, ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, ZB_ZCL_CLUSTER_ID_IDENTIFY, ZB_ZCL_IDENTIFY_ATTRS_T, AnalogAttrs, + AnalogAttrsOutput, BinaryAttrs, ZigbeeComponent, zigbee_ns, @@ -76,6 +79,7 @@ from .const_zephyr import ( ZigbeeBinarySensor = zigbee_ns.class_("ZigbeeBinarySensor", cg.Component) ZigbeeSensor = zigbee_ns.class_("ZigbeeSensor", cg.Component) ZigbeeSwitch = zigbee_ns.class_("ZigbeeSwitch", cg.Component) +ZigbeeNumber = zigbee_ns.class_("ZigbeeNumber", cg.Component) # BACnet engineering units mapping (ZCL uses BACnet unit codes) # See: https://github.com/zigpy/zha/blob/dev/zha/application/platforms/number/bacnet.py @@ -139,6 +143,15 @@ zephyr_switch = cv.Schema( } ) +zephyr_number = cv.Schema( + { + cv.OnlyWith(CONF_ZIGBEE_ID, ["nrf52", "zigbee"]): cv.use_id(ZigbeeComponent), + cv.OnlyWith(CONF_ZIGBEE_NUMBER, ["nrf52", "zigbee"]): cv.declare_id( + ZigbeeNumber + ), + } +) + async def zephyr_to_code(config: ConfigType) -> None: zephyr_add_prj_conf("ZIGBEE", True) @@ -344,6 +357,16 @@ async def zephyr_setup_switch(entity: cg.MockObj, config: ConfigType) -> None: CORE.add_job(_add_switch, entity, config) +async def zephyr_setup_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + CORE.add_job(_add_number, entity, config, min_value, max_value, step) + + def get_slot_index() -> int: """Find the next available endpoint slot.""" slot = next( @@ -451,3 +474,31 @@ async def _add_switch(entity: cg.MockObj, config: ConfigType) -> None: ZB_ZCL_CLUSTER_ID_BINARY_OUTPUT, "ZB_HA_CUSTOM_ATTR_DEVICE_ID", ) + + +async def _add_number( + entity: cg.MockObj, + config: ConfigType, + min_value: float, + max_value: float, + step: float, +) -> None: + # Get BACnet engineering unit from unit_of_measurement + unit = config.get(CONF_UNIT_OF_MEASUREMENT, "") + bacnet_unit = BACNET_UNITS.get(unit, BACNET_UNIT_NO_UNITS) + + await _add_zigbee_ep( + entity, + config, + CONF_ZIGBEE_NUMBER, + AnalogAttrsOutput, + "ESPHOME_ZB_ZCL_DECLARE_ANALOG_OUTPUT_ATTRIB_LIST", + ZB_ZCL_CLUSTER_ID_ANALOG_OUTPUT, + "ZB_HA_CUSTOM_ATTR_DEVICE_ID", + extra_field_values={ + "max_present_value": max_value, + "min_present_value": min_value, + "resolution": step, + "engineering_units": bacnet_unit, + }, + ) diff --git a/esphome/config_validation.py b/esphome/config_validation.py index b7ab02013d..a9d1a72e5a 100644 --- a/esphome/config_validation.py +++ b/esphome/config_validation.py @@ -682,7 +682,7 @@ def only_with_framework( def validator_(obj): if CORE.target_framework not in frameworks: err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}" - if suggestion := suggestions.get(CORE.target_framework, None): + if suggestion := suggestions.get(CORE.target_framework): (component, docs_path) = suggestion err_str += f"\nPlease use '{component}'" if docs_path: @@ -1403,6 +1403,17 @@ def requires_component(comp): return validator +def conflicts_with_component(comp): + """Validate that this option cannot be specified when the component `comp` is loaded.""" + + def validator(value): + if comp in CORE.loaded_integrations: + raise Invalid(f"This option is not compatible with component {comp}") + return value + + return validator + + uint8_t = int_range(min=0, max=255) uint16_t = int_range(min=0, max=65535) uint32_t = int_range(min=0, max=4294967295) diff --git a/esphome/const.py b/esphome/const.py index 4243b2e25d..4bf47b8f83 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -149,6 +149,7 @@ CONF_ASSUMED_STATE = "assumed_state" CONF_AT = "at" CONF_ATTENUATION = "attenuation" CONF_ATTRIBUTE = "attribute" +CONF_AUDIO_DAC = "audio_dac" CONF_AUTH = "auth" CONF_AUTO_CLEAR_ENABLED = "auto_clear_enabled" CONF_AUTO_MODE = "auto_mode" diff --git a/esphome/core/__init__.py b/esphome/core/__init__.py index 9a7dd49609..484f679369 100644 --- a/esphome/core/__init__.py +++ b/esphome/core/__init__.py @@ -278,9 +278,13 @@ LAMBDA_PROG = re.compile(r"\bid\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)") class Lambda: def __init__(self, value): + from esphome.cpp_generator import Expression, statement + # pylint: disable=protected-access if isinstance(value, Lambda): self._value = value._value + elif isinstance(value, Expression): + self._value = str(statement(value)) else: self._value = value self._parts = None @@ -888,6 +892,16 @@ class EsphomeCore: library.name if "/" not in library.name else library.name.split("/")[-1] ) + # Auto-enable Arduino libraries on ESP32 Arduino builds + if self.is_esp32 and self.using_arduino: + from esphome.components.esp32 import ( + ARDUINO_DISABLED_LIBRARIES, + _enable_arduino_library, + ) + + if short_name in ARDUINO_DISABLED_LIBRARIES: + _enable_arduino_library(short_name) + if short_name not in self.platformio_libraries: _LOGGER.debug("Adding library: %s", library) self.platformio_libraries[short_name] = library diff --git a/esphome/core/application.cpp b/esphome/core/application.cpp index 55eb25ce09..0daabc0282 100644 --- a/esphome/core/application.cpp +++ b/esphome/core/application.cpp @@ -81,6 +81,10 @@ void Application::register_component_(Component *comp) { return; } } + if (this->components_.size() >= ESPHOME_COMPONENT_COUNT) { + ESP_LOGE(TAG, "Cannot register component %s - at capacity!", LOG_STR_ARG(comp->get_component_log_str())); + return; + } this->components_.push_back(comp); } void Application::setup() { @@ -210,7 +214,7 @@ void Application::loop() { #ifdef USE_ESP32 esp_chip_info_t chip_info; esp_chip_info(&chip_info); - ESP_LOGI(TAG, "ESP32 Chip: %s r%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, + ESP_LOGI(TAG, "ESP32 Chip: %s rev%d.%d, %d core(s)", ESPHOME_VARIANT, chip_info.revision / 100, chip_info.revision % 100, chip_info.cores); #if defined(USE_ESP32_VARIANT_ESP32) && !defined(USE_ESP32_MIN_CHIP_REVISION_SET) // Suggest optimization for chips that don't need the PSRAM cache workaround diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 98e8c02d07..f09a39d2bb 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -359,6 +359,10 @@ void Component::defer(const std::string &name, std::function<void()> &&f) { // void Component::defer(const char *name, std::function<void()> &&f) { // NOLINT App.scheduler.set_timeout(this, name, 0, std::move(f)); } +void Component::defer(uint32_t id, std::function<void()> &&f) { // NOLINT + App.scheduler.set_timeout(this, id, 0, std::move(f)); +} +bool Component::cancel_defer(uint32_t id) { return App.scheduler.cancel_timeout(this, id); } void Component::set_timeout(uint32_t timeout, std::function<void()> &&f) { // NOLINT App.scheduler.set_timeout(this, static_cast<const char *>(nullptr), timeout, std::move(f)); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 49349d4199..97f2afe1a4 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -494,11 +494,15 @@ class Component { /// Defer a callback to the next loop() call. void defer(std::function<void()> &&f); // NOLINT + /// Defer a callback with a numeric ID (zero heap allocation) + void defer(uint32_t id, std::function<void()> &&f); // NOLINT + /// Cancel a defer callback using the specified name, name must not be empty. // Remove before 2026.7.0 ESPDEPRECATED("Use const char* overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_defer(const std::string &name); // NOLINT bool cancel_defer(const char *name); // NOLINT + bool cancel_defer(uint32_t id); // NOLINT // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index e98cdd0ba0..ee865a7e65 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -227,6 +227,8 @@ #define USE_WIFI_SCAN_RESULTS_LISTENERS #define USE_WIFI_CONNECT_STATE_LISTENERS #define USE_WIFI_POWER_SAVE_LISTENERS +#define USE_WIFI_CONNECT_TRIGGER +#define USE_WIFI_DISCONNECT_TRIGGER #define ESPHOME_WIFI_IP_STATE_LISTENERS 2 #define ESPHOME_WIFI_SCAN_RESULTS_LISTENERS 2 #define ESPHOME_WIFI_CONNECT_STATE_LISTENERS 2 @@ -240,6 +242,8 @@ #define USE_ETHERNET_KSZ8081 #define USE_ETHERNET_MANUAL_IP #define USE_ETHERNET_IP_STATE_LISTENERS +#define USE_ETHERNET_CONNECT_TRIGGER +#define USE_ETHERNET_DISCONNECT_TRIGGER #define ESPHOME_ETHERNET_IP_STATE_LISTENERS 2 #endif diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 1a5d22f8d8..c2f7f67d9a 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -295,7 +295,7 @@ size_t parse_hex(const char *str, size_t length, uint8_t *data, size_t count) { size_t chars = std::min(length, 2 * count); for (size_t i = 2 * count - chars; i < 2 * count; i++, str++) { uint8_t val = parse_hex_char(*str); - if (val > 15) + if (val == INVALID_HEX_CHAR) return 0; data[i >> 1] = (i & 1) ? data[i >> 1] | val : val << 4; } diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 9c7060cd1d..f7de34b6d5 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -874,6 +874,9 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> optional< } /// Parse a hex character to its nibble value (0-15), returns 255 on invalid input +/// Returned by parse_hex_char() for non-hex characters. +static constexpr uint8_t INVALID_HEX_CHAR = 255; + constexpr uint8_t parse_hex_char(char c) { if (c >= '0' && c <= '9') return c - '0'; @@ -881,7 +884,7 @@ constexpr uint8_t parse_hex_char(char c) { return c - 'A' + 10; if (c >= 'a' && c <= 'f') return c - 'a' + 10; - return 255; + return INVALID_HEX_CHAR; } /// Convert a nibble (0-15) to hex char with specified base ('a' for lowercase, 'A' for uppercase) diff --git a/esphome/core/progmem.h b/esphome/core/progmem.h index 4b897fb2de..6c6a5252cf 100644 --- a/esphome/core/progmem.h +++ b/esphome/core/progmem.h @@ -1,5 +1,11 @@ #pragma once +#include <array> +#include <cstddef> +#include <cstdint> + +#include "esphome/core/hal.h" // For PROGMEM definition + // Platform-agnostic macros for PROGMEM string handling // On ESP8266/Arduino: Use Arduino's F() macro for PROGMEM strings // On other platforms: Use plain strings (no PROGMEM) @@ -32,3 +38,80 @@ using ProgmemStr = const __FlashStringHelper *; // Type for pointers to strings (no PROGMEM on non-ESP8266 platforms) using ProgmemStr = const char *; #endif + +namespace esphome { + +/// Helper for C++20 string literal template arguments +template<size_t N> struct FixedString { + char data[N]{}; + constexpr FixedString(const char (&str)[N]) { + for (size_t i = 0; i < N; ++i) + data[i] = str[i]; + } + constexpr size_t size() const { return N - 1; } // exclude null terminator +}; + +/// Compile-time string table that packs strings into a single blob with offset lookup. +/// Use PROGMEM_STRING_TABLE macro to instantiate with proper flash placement on ESP8266. +/// +/// Example: +/// PROGMEM_STRING_TABLE(MyStrings, "foo", "bar", "baz"); +/// ProgmemStr str = MyStrings::get_progmem_str(idx, MyStrings::LAST_INDEX); // For ArduinoJson +/// const LogString *log_str = MyStrings::get_log_str(idx, MyStrings::LAST_INDEX); // For logging +/// +template<FixedString... Strs> struct ProgmemStringTable { + static constexpr size_t COUNT = sizeof...(Strs); + static constexpr size_t BLOB_SIZE = (0 + ... + (Strs.size() + 1)); + + /// Generate packed string blob at compile time + static constexpr auto make_blob() { + std::array<char, BLOB_SIZE> result{}; + size_t pos = 0; + auto copy = [&](const auto &str) { + for (size_t i = 0; i <= str.size(); ++i) + result[pos++] = str.data[i]; + }; + (copy(Strs), ...); + return result; + } + + /// Generate offset table at compile time (uint8_t limits blob to 255 bytes) + static constexpr auto make_offsets() { + static_assert(COUNT > 0, "PROGMEM_STRING_TABLE must contain at least one string"); + static_assert(COUNT <= 255, "PROGMEM_STRING_TABLE supports at most 255 strings with uint8_t indices"); + static_assert(BLOB_SIZE <= 255, "PROGMEM_STRING_TABLE blob exceeds 255 bytes; use fewer/shorter strings"); + std::array<uint8_t, COUNT> result{}; + size_t pos = 0, idx = 0; + ((result[idx++] = static_cast<uint8_t>(pos), pos += Strs.size() + 1), ...); + return result; + } +}; + +// Forward declaration for LogString (defined in log.h) +struct LogString; + +/// Instantiate a ProgmemStringTable with PROGMEM storage. +/// Creates: Name::get_progmem_str(idx, fallback), Name::get_log_str(idx, fallback) +/// If idx >= COUNT, returns string at fallback. Use LAST_INDEX for common patterns. +#define PROGMEM_STRING_TABLE(Name, ...) \ + struct Name { \ + using Table = ::esphome::ProgmemStringTable<__VA_ARGS__>; \ + static constexpr size_t COUNT = Table::COUNT; \ + static constexpr uint8_t LAST_INDEX = COUNT - 1; \ + static constexpr size_t BLOB_SIZE = Table::BLOB_SIZE; \ + static constexpr auto BLOB PROGMEM = Table::make_blob(); \ + static constexpr auto OFFSETS PROGMEM = Table::make_offsets(); \ + static const char *get_(uint8_t idx, uint8_t fallback) { \ + if (idx >= COUNT) \ + idx = fallback; \ + return &BLOB[::esphome::progmem_read_byte(&OFFSETS[idx])]; \ + } \ + static ::ProgmemStr get_progmem_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<::ProgmemStr>(get_(idx, fallback)); \ + } \ + static const ::esphome::LogString *get_log_str(uint8_t idx, uint8_t fallback) { \ + return reinterpret_cast<const ::esphome::LogString *>(get_(idx, fallback)); \ + } \ + } + +} // namespace esphome diff --git a/esphome/cpp_generator.py b/esphome/cpp_generator.py index cff0748c95..83f2d6cf81 100644 --- a/esphome/cpp_generator.py +++ b/esphome/cpp_generator.py @@ -462,6 +462,16 @@ def statement(expression: Expression | Statement) -> Statement: return ExpressionStatement(expression) +def literal(name: str) -> "MockObj": + """Create a literal name that will appear in the generated code + not surrounded by quotes. + + :param name: The name of the literal. + :return: The literal as a MockObj. + """ + return MockObj(name, "") + + def variable( id_: ID, rhs: SafeExpType, type_: "MockObj" = None, register=True ) -> "MockObj": @@ -665,7 +675,7 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]: async def process_lambda( - value: Lambda, + value: Lambda | Expression, parameters: TemplateArgsType, capture: str = "", return_type: SafeExpType = None, @@ -689,6 +699,14 @@ async def process_lambda( if value is None: return None + # Inadvertently passing a malformed parameters value will lead to the build process mysteriously hanging at the + # "Generating C++ source..." stage, so check here to save the developer's hair. + assert isinstance(parameters, list) and all( + isinstance(p, tuple) and len(p) == 2 for p in parameters + ) + if isinstance(value, Expression): + value = Lambda(value) + parts = value.parts[:] for i, id in enumerate(value.requires_ids): full_id, var = await get_variable_with_full_id(id) diff --git a/esphome/dashboard/dns.py b/esphome/dashboard/dns.py index 58867f7bc1..eb4a87dbfb 100644 --- a/esphome/dashboard/dns.py +++ b/esphome/dashboard/dns.py @@ -3,11 +3,16 @@ from __future__ import annotations import asyncio from contextlib import suppress from ipaddress import ip_address +import logging from icmplib import NameLookupError, async_resolve RESOLVE_TIMEOUT = 3.0 +_LOGGER = logging.getLogger(__name__) + +_RESOLVE_EXCEPTIONS = (TimeoutError, NameLookupError, UnicodeError) + async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: """Wrap the icmplib async_resolve function.""" @@ -16,7 +21,21 @@ async def _async_resolve_wrapper(hostname: str) -> list[str] | Exception: try: async with asyncio.timeout(RESOLVE_TIMEOUT): return await async_resolve(hostname) - except (TimeoutError, NameLookupError, UnicodeError) as ex: + except _RESOLVE_EXCEPTIONS as ex: + # If the hostname ends with .local and resolution failed, + # try the bare hostname as a fallback since mDNS may not be + # working on the system but unicast DNS might resolve it + if hostname.endswith(".local"): + bare_hostname = hostname[:-6] # Remove ".local" + try: + async with asyncio.timeout(RESOLVE_TIMEOUT): + result = await async_resolve(bare_hostname) + _LOGGER.debug( + "Bare hostname %s resolved to %s", bare_hostname, result + ) + return result + except _RESOLVE_EXCEPTIONS: + _LOGGER.debug("Bare hostname %s also failed to resolve", bare_hostname) return ex diff --git a/esphome/enum.py b/esphome/enum.py index 0fe30cf92a..cf0d8b645b 100644 --- a/esphome/enum.py +++ b/esphome/enum.py @@ -2,19 +2,7 @@ from __future__ import annotations -from enum import Enum -from typing import Any +from enum import StrEnum as _StrEnum - -class StrEnum(str, Enum): - """Partial backport of Python 3.11's StrEnum for our basic use cases.""" - - def __new__(cls, value: str, *args: Any, **kwargs: Any) -> StrEnum: - """Create a new StrEnum instance.""" - if not isinstance(value, str): - raise TypeError(f"{value!r} is not a string") - return super().__new__(cls, value, *args, **kwargs) - - def __str__(self) -> str: - """Return self.value.""" - return str(self.value) +# Re-export StrEnum from standard library for backwards compatibility +StrEnum = _StrEnum diff --git a/esphome/wizard.py b/esphome/wizard.py index d77450b04d..4b74847996 100644 --- a/esphome/wizard.py +++ b/esphome/wizard.py @@ -1,5 +1,7 @@ +import base64 from pathlib import Path import random +import secrets import string from typing import Literal, NotRequired, TypedDict, Unpack import unicodedata @@ -116,7 +118,6 @@ class WizardFileKwargs(TypedDict): board: str ssid: NotRequired[str] psk: NotRequired[str] - password: NotRequired[str] ota_password: NotRequired[str] api_encryption_key: NotRequired[str] friendly_name: NotRequired[str] @@ -144,9 +145,7 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: config += API_CONFIG - # Configure API - if "password" in kwargs: - config += f' password: "{kwargs["password"]}"\n' + # Configure API encryption if "api_encryption_key" in kwargs: config += f' encryption:\n key: "{kwargs["api_encryption_key"]}"\n' @@ -155,8 +154,6 @@ def wizard_file(**kwargs: Unpack[WizardFileKwargs]) -> str: config += " - platform: esphome\n" if "ota_password" in kwargs: config += f' password: "{kwargs["ota_password"]}"' - elif "password" in kwargs: - config += f' password: "{kwargs["password"]}"' # Configuring wifi config += "\n\nwifi:\n" @@ -205,7 +202,6 @@ class WizardWriteKwargs(TypedDict): platform: NotRequired[str] ssid: NotRequired[str] psk: NotRequired[str] - password: NotRequired[str] ota_password: NotRequired[str] api_encryption_key: NotRequired[str] friendly_name: NotRequired[str] @@ -232,7 +228,7 @@ def wizard_write(path: Path, **kwargs: Unpack[WizardWriteKwargs]) -> bool: else: # "basic" board = kwargs["board"] - for key in ("ssid", "psk", "password", "ota_password"): + for key in ("ssid", "psk", "ota_password"): if key in kwargs: kwargs[key] = sanitize_double_quotes(kwargs[key]) if "platform" not in kwargs: @@ -474,7 +470,7 @@ def wizard(path: Path) -> int: sleep(1) # Do not create wifi if the board does not support it - if board not in ["rpipico"]: + if board != "rpipico": safe_print_step(3, WIFI_BIG) safe_print("In this step, I'm going to create the configuration for WiFi.") safe_print() @@ -522,26 +518,54 @@ def wizard(path: Path) -> int: "Almost there! ESPHome can automatically upload custom firmwares over WiFi " "(over the air) and integrates into Home Assistant with a native API." ) + safe_print() + sleep(0.5) + + # Generate encryption key (32 bytes, base64 encoded) for secure API communication + noise_psk = secrets.token_bytes(32) + api_encryption_key = base64.b64encode(noise_psk).decode() + safe_print( - f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?" + "For secure API communication, I've generated a random encryption key." + ) + safe_print() + safe_print( + f"Your {color(AnsiFore.GREEN, 'API encryption key')} is: " + f"{color(AnsiFore.BOLD_WHITE, api_encryption_key)}" + ) + safe_print() + safe_print("You'll need this key when adding the device to Home Assistant.") + sleep(1) + + safe_print() + safe_print( + f"Do you want to set a {color(AnsiFore.GREEN, 'password')} for OTA updates? " + "This can be insecure if you do not trust the WiFi network." ) safe_print() sleep(0.25) safe_print("Press ENTER for no password") - password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) + ota_password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): ")) else: - ssid, password, psk = "", "", "" + ssid, psk = "", "" + api_encryption_key = None + ota_password = "" - if not wizard_write( - path=path, - name=name, - platform=platform, - board=board, - ssid=ssid, - psk=psk, - password=password, - type="basic", - ): + kwargs = { + "path": path, + "name": name, + "platform": platform, + "board": board, + "ssid": ssid, + "psk": psk, + "type": "basic", + } + if api_encryption_key: + kwargs["api_encryption_key"] = api_encryption_key + if ota_password: + kwargs["ota_password"] = ota_password + + if not wizard_write(**kwargs): return 1 safe_print() diff --git a/esphome/writer.py b/esphome/writer.py index cb9c921693..661118e518 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -421,6 +421,11 @@ def _rmtree_error_handler( func(path) +def rmtree(path: Path | str) -> None: + """Remove a directory tree, handling read-only files on Windows.""" + shutil.rmtree(path, onerror=_rmtree_error_handler) + + def clean_build(clear_pio_cache: bool = True): # Allow skipping cache cleaning for integration tests if os.environ.get("ESPHOME_SKIP_CLEAN_BUILD"): @@ -430,11 +435,11 @@ def clean_build(clear_pio_cache: bool = True): pioenvs = CORE.relative_pioenvs_path() if pioenvs.is_dir(): _LOGGER.info("Deleting %s", pioenvs) - shutil.rmtree(pioenvs, onerror=_rmtree_error_handler) + rmtree(pioenvs) piolibdeps = CORE.relative_piolibdeps_path() if piolibdeps.is_dir(): _LOGGER.info("Deleting %s", piolibdeps) - shutil.rmtree(piolibdeps, onerror=_rmtree_error_handler) + rmtree(piolibdeps) dependencies_lock = CORE.relative_build_path("dependencies.lock") if dependencies_lock.is_file(): _LOGGER.info("Deleting %s", dependencies_lock) @@ -455,7 +460,7 @@ def clean_build(clear_pio_cache: bool = True): cache_dir = Path(config.get("platformio", "cache_dir")) if cache_dir.is_dir(): _LOGGER.info("Deleting PlatformIO cache %s", cache_dir) - shutil.rmtree(cache_dir, onerror=_rmtree_error_handler) + rmtree(cache_dir) def clean_all(configuration: list[str]): @@ -480,7 +485,7 @@ def clean_all(configuration: list[str]): if item.is_file() and not item.name.endswith(".json"): item.unlink() elif item.is_dir() and item.name != "storage": - shutil.rmtree(item, onerror=_rmtree_error_handler) + rmtree(item) # Clean PlatformIO project files try: @@ -494,7 +499,7 @@ def clean_all(configuration: list[str]): path = Path(config.get("platformio", pio_dir)) if path.is_dir(): _LOGGER.info("Deleting PlatformIO %s %s", pio_dir, path) - shutil.rmtree(path, onerror=_rmtree_error_handler) + rmtree(path) GITIGNORE_CONTENT = """# Gitignore settings for ESPHome diff --git a/platformio.ini b/platformio.ini index 0f5bf2f8fb..bb0de3c2b1 100644 --- a/platformio.ini +++ b/platformio.ini @@ -114,7 +114,7 @@ lib_deps = ESP8266WiFi ; wifi (Arduino built-in) Update ; ota (Arduino built-in) ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base makuna/NeoPixelBus@2.7.3 ; neopixelbus ESP8266HTTPClient ; http_request (Arduino built-in) ESP8266mDNS ; mdns (Arduino built-in) @@ -202,7 +202,7 @@ lib_deps = ${common:arduino.lib_deps} ayushsharma82/RPAsyncTCP@1.3.2 ; async_tcp bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base build_flags = ${common:arduino.build_flags} -DUSE_RP2040 @@ -218,7 +218,7 @@ framework = arduino lib_compat_mode = soft lib_deps = bblanchon/ArduinoJson@7.4.2 ; json - ESP32Async/ESPAsyncWebServer@3.9.5 ; web_server_base + ESP32Async/ESPAsyncWebServer@3.9.6 ; web_server_base droscy/esp_wireguard@0.4.2 ; wireguard build_flags = ${common:arduino.build_flags} diff --git a/requirements.txt b/requirements.txt index 821324262b..bb118c5f9a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ tornado==6.5.4 tzlocal==5.3.1 # from time tzdata>=2021.1 # from time pyserial==3.5 -platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile +platformio==6.1.19 esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 diff --git a/requirements_test.txt b/requirements_test.txt index 5d90764021..2cf6f6456e 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.4 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.14.14 # also change in .pre-commit-config.yaml when updating +ruff==0.15.0 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 7625458f9f..4021a062ca 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -280,7 +280,7 @@ class TypeInfo(ABC): """ field_id_size = self.calculate_field_id_size() method = f"{base_method}_force" if force else base_method - value = value_expr if value_expr else name + value = value_expr or name return f"size.{method}({field_id_size}, {value});" @abstractmethod @@ -2599,15 +2599,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name, out.append(indent, ' ').append(field_name).append(": "); } -static inline void append_with_newline(DumpBuffer &out, const char *str) { - out.append(str); - out.append("\\n"); -} - static inline void append_uint(DumpBuffer &out, uint32_t value) { - char buf[16]; - snprintf(buf, sizeof(buf), "%" PRIu32, value); - out.append(buf); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value)); } // RAII helper for message dump formatting @@ -2625,31 +2618,23 @@ class MessageDumpHelper { // Helper functions to reduce code duplication in dump methods static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRId32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu32, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%g", value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) { - char buffer[64]; append_field_prefix(out, field_name, indent); - snprintf(buffer, 64, "%" PRIu64, value); - append_with_newline(out, buffer); + out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\\n", value)); } static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) { @@ -2689,7 +2674,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint char hex_buf[format_hex_pretty_size(160)]; append_field_prefix(out, field_name, indent); format_hex_pretty_to(hex_buf, data, len); - append_with_newline(out, hex_buf); + out.append(hex_buf).append("\\n"); } """ @@ -2863,7 +2848,7 @@ static const char *const TAG = "api.service"; hpp += " DumpBuffer dump_buf;\n" hpp += " this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));\n" hpp += "#endif\n" - hpp += " return this->send_message_(msg, message_type);\n" + hpp += " return this->send_message_impl(msg, message_type);\n" hpp += " }\n\n" # Add logging helper method implementations to cpp diff --git a/script/ci-custom.py b/script/ci-custom.py index b146c9867e..b5bec74fa7 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -756,6 +756,28 @@ def lint_no_sprintf(fname, match): ) +@lint_re_check( + # Match scanf family functions: scanf, sscanf, fscanf, vscanf, vsscanf, vfscanf + # Also match std:: prefixed versions + # [^\w] ensures we match function calls, not substrings + r"[^\w]((?:std::)?v?[fs]?scanf)\s*\(" + CPP_RE_EOL, + include=cpp_include, +) +def lint_no_scanf(fname, match): + func = match.group(1) + return ( + f"{highlight(func + '()')} is not allowed in new ESPHome code. The scanf family " + f"pulls in ~7KB flash on ESP8266 and ~9KB on ESP32, and ESPHome doesn't otherwise " + f"need this code.\n" + f"Please use alternatives:\n" + f" - {highlight('parse_number<T>(str)')} for parsing integers/floats from strings\n" + f" - {highlight('strtol()/strtof()')} for C-style number parsing with error checking\n" + f" - {highlight('parse_hex()')} for hex string parsing\n" + f" - Manual parsing for simple fixed formats\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], diff --git a/script/merge_component_configs.py b/script/merge_component_configs.py index 59774edba9..5e98f1fef5 100755 --- a/script/merge_component_configs.py +++ b/script/merge_component_configs.py @@ -249,7 +249,7 @@ def merge_component_configs( if all_packages is None: # First component - initialize package dict - all_packages = comp_packages if comp_packages else {} + all_packages = comp_packages or {} elif comp_packages: # Merge packages - combine all unique package types # If both have the same package type, verify they're identical diff --git a/tests/component_tests/config_validation/test_config.py b/tests/component_tests/config_validation/test_config.py index 1a9b9bc1f3..25d85d333b 100644 --- a/tests/component_tests/config_validation/test_config.py +++ b/tests/component_tests/config_validation/test_config.py @@ -1,10 +1,14 @@ """ -Test schema.extend functionality in esphome.config_validation. +Test config_validation functionality in esphome.config_validation. """ from typing import Any +import pytest +from voluptuous import Invalid + import esphome.config_validation as cv +from esphome.core import CORE def test_config_extend() -> None: @@ -49,3 +53,37 @@ def test_config_extend() -> None: assert validated["key2"] == "initial_value2" assert validated["extra_1"] == "value1" assert validated["extra_2"] == "value2" + + +def test_requires_component_passes_when_loaded() -> None: + """Test requires_component passes when the required component is loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.requires_component("wifi") + result = validator("test_value") + assert result == "test_value" + + +def test_requires_component_fails_when_not_loaded() -> None: + """Test requires_component raises Invalid when the required component is not loaded.""" + CORE.loaded_integrations.add("logger") + validator = cv.requires_component("wifi") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "requires component wifi" in str(exc_info.value) + + +def test_conflicts_with_component_passes_when_not_loaded() -> None: + """Test conflicts_with_component passes when the conflicting component is not loaded.""" + CORE.loaded_integrations.update({"wifi", "logger"}) + validator = cv.conflicts_with_component("esp32_hosted") + result = validator("test_value") + assert result == "test_value" + + +def test_conflicts_with_component_fails_when_loaded() -> None: + """Test conflicts_with_component raises Invalid when the conflicting component is loaded.""" + CORE.loaded_integrations.update({"wifi", "esp32_hosted"}) + validator = cv.conflicts_with_component("esp32_hosted") + with pytest.raises(Invalid) as exc_info: + validator("test_value") + assert "not compatible with component esp32_hosted" in str(exc_info.value) diff --git a/tests/component_tests/epaper_spi/test_init.py b/tests/component_tests/epaper_spi/test_init.py index 71e66cd043..a9f5735fca 100644 --- a/tests/component_tests/epaper_spi/test_init.py +++ b/tests/component_tests/epaper_spi/test_init.py @@ -289,3 +289,56 @@ def test_model_with_full_update_every( "full_update_every": 10, } ) + + +def test_busy_pin_input_mode_ssd1677( + set_core_config: SetCoreConfigCallable, + set_component_config: Callable[[str, Any], None], +) -> None: + """Test that busy_pin has input mode and cs/dc/reset pins have output mode for ssd1677.""" + set_core_config( + PlatformFramework.ESP32_IDF, + platform_data={KEY_BOARD: "esp32dev", KEY_VARIANT: VARIANT_ESP32}, + ) + + # Configure SPI component which is required by epaper_spi + set_component_config("spi", {"id": "spi_bus", "clk_pin": 18, "mosi_pin": 19}) + + result = run_schema_validation( + { + "id": "test_display", + "model": "ssd1677", + "dc_pin": 21, + "busy_pin": 22, + "reset_pin": 23, + "cs_pin": 5, + "dimensions": { + "width": 200, + "height": 200, + }, + } + ) + + # Verify that busy_pin has input mode set + assert CONF_BUSY_PIN in result + busy_pin_config = result[CONF_BUSY_PIN] + assert "mode" in busy_pin_config + assert busy_pin_config["mode"]["input"] is True + + # Verify that cs_pin has output mode set + assert CONF_CS_PIN in result + cs_pin_config = result[CONF_CS_PIN] + assert "mode" in cs_pin_config + assert cs_pin_config["mode"]["output"] is True + + # Verify that dc_pin has output mode set + assert CONF_DC_PIN in result + dc_pin_config = result[CONF_DC_PIN] + assert "mode" in dc_pin_config + assert dc_pin_config["mode"]["output"] is True + + # Verify that reset_pin has output mode set + assert CONF_RESET_PIN in result + reset_pin_config = result[CONF_RESET_PIN] + assert "mode" in reset_pin_config + assert reset_pin_config["mode"]["output"] is True diff --git a/tests/components/adc/test.esp32-c2-idf.yaml b/tests/components/adc/test.esp32-c2-idf.yaml new file mode 100644 index 0000000000..e764f0fe21 --- /dev/null +++ b/tests/components/adc/test.esp32-c2-idf.yaml @@ -0,0 +1,12 @@ +sensor: + - id: my_sensor + platform: adc + pin: GPIO1 + name: ADC Test sensor + update_interval: "1:01" + attenuation: 2.5db + unit_of_measurement: "°C" + icon: "mdi:water-percent" + accuracy_decimals: 5 + setup_priority: -100 + force_update: true diff --git a/tests/components/ch423/common.yaml b/tests/components/ch423/common.yaml new file mode 100644 index 0000000000..ccf9170bd0 --- /dev/null +++ b/tests/components/ch423/common.yaml @@ -0,0 +1,36 @@ +ch423: + - id: ch423_hub + i2c_id: i2c_bus + +binary_sensor: + - platform: gpio + id: ch423_input + name: CH423 Binary Sensor + pin: + ch423: ch423_hub + number: 1 + mode: INPUT + inverted: true + - platform: gpio + id: ch423_input_2 + name: CH423 Binary Sensor 2 + pin: + ch423: ch423_hub + number: 0 + mode: INPUT + inverted: false +output: + - platform: gpio + id: ch423_out_11 + pin: + ch423: ch423_hub + number: 11 + mode: OUTPUT_OPEN_DRAIN + inverted: true + - platform: gpio + id: ch423_out_23 + pin: + ch423: ch423_hub + number: 23 + mode: OUTPUT_OPEN_DRAIN + inverted: false diff --git a/tests/components/ch423/test.esp32-idf.yaml b/tests/components/ch423/test.esp32-idf.yaml new file mode 100644 index 0000000000..b47e39c389 --- /dev/null +++ b/tests/components/ch423/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp32-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/ch423/test.esp8266-ard.yaml b/tests/components/ch423/test.esp8266-ard.yaml new file mode 100644 index 0000000000..4a98b9388a --- /dev/null +++ b/tests/components/ch423/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/ch423/test.rp2040-ard.yaml b/tests/components/ch423/test.rp2040-ard.yaml new file mode 100644 index 0000000000..319a7c71a6 --- /dev/null +++ b/tests/components/ch423/test.rp2040-ard.yaml @@ -0,0 +1,4 @@ +packages: + i2c: !include ../../test_build_components/common/i2c/rp2040-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/dlms_meter/common-generic.yaml b/tests/components/dlms_meter/common-generic.yaml new file mode 100644 index 0000000000..edb1c66f0f --- /dev/null +++ b/tests/components/dlms_meter/common-generic.yaml @@ -0,0 +1,11 @@ +dlms_meter: + decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key! + +sensor: + - platform: dlms_meter + reactive_energy_plus: + name: "Reactive energy taken from grid" + reactive_energy_minus: + name: "Reactive energy put into grid" + +<<: !include common.yaml diff --git a/tests/components/dlms_meter/common-netznoe.yaml b/tests/components/dlms_meter/common-netznoe.yaml new file mode 100644 index 0000000000..db064b64f9 --- /dev/null +++ b/tests/components/dlms_meter/common-netznoe.yaml @@ -0,0 +1,17 @@ +dlms_meter: + decryption_key: "36C66639E48A8CA4D6BC8B282A793BBB" # change this to your decryption key! + provider: netznoe # (optional) key - only set if using evn + +sensor: + - platform: dlms_meter + # EVN + power_factor: + name: "Power Factor" + +text_sensor: + - platform: dlms_meter + # EVN + meternumber: + name: "meterNumber" + +<<: !include common.yaml diff --git a/tests/components/dlms_meter/common.yaml b/tests/components/dlms_meter/common.yaml new file mode 100644 index 0000000000..6aa4e1b0ff --- /dev/null +++ b/tests/components/dlms_meter/common.yaml @@ -0,0 +1,27 @@ +sensor: + - platform: dlms_meter + voltage_l1: + name: "Voltage L1" + voltage_l2: + name: "Voltage L2" + voltage_l3: + name: "Voltage L3" + current_l1: + name: "Current L1" + current_l2: + name: "Current L2" + current_l3: + name: "Current L3" + active_power_plus: + name: "Active power taken from grid" + active_power_minus: + name: "Active power put into grid" + active_energy_plus: + name: "Active energy taken from grid" + active_energy_minus: + name: "Active energy put into grid" + +text_sensor: + - platform: dlms_meter + timestamp: + name: "timestamp" diff --git a/tests/components/dlms_meter/test.esp32-ard.yaml b/tests/components/dlms_meter/test.esp32-ard.yaml new file mode 100644 index 0000000000..c9910aa600 --- /dev/null +++ b/tests/components/dlms_meter/test.esp32-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-ard.yaml + +<<: !include common-generic.yaml diff --git a/tests/components/dlms_meter/test.esp32-idf.yaml b/tests/components/dlms_meter/test.esp32-idf.yaml new file mode 100644 index 0000000000..1547532f1e --- /dev/null +++ b/tests/components/dlms_meter/test.esp32-idf.yaml @@ -0,0 +1,4 @@ +packages: + uart_2400: !include ../../test_build_components/common/uart_2400/esp32-idf.yaml + +<<: !include common-netznoe.yaml diff --git a/tests/components/dlms_meter/test.esp8266-ard.yaml b/tests/components/dlms_meter/test.esp8266-ard.yaml new file mode 100644 index 0000000000..119a1978de --- /dev/null +++ b/tests/components/dlms_meter/test.esp8266-ard.yaml @@ -0,0 +1,4 @@ +packages: + uart_2400: !include ../../test_build_components/common/uart_2400/esp8266-ard.yaml + +<<: !include common-generic.yaml diff --git a/tests/components/esp32/test.esp32-idf.yaml b/tests/components/esp32/test.esp32-idf.yaml index d38cdfe2fd..f80c854de5 100644 --- a/tests/components/esp32/test.esp32-idf.yaml +++ b/tests/components/esp32/test.esp32-idf.yaml @@ -8,6 +8,16 @@ esp32: enable_lwip_bridge_interface: true disable_libc_locks_in_iram: false # Test explicit opt-out of RAM optimization use_full_certificate_bundle: false # Test CMN bundle (default) + include_builtin_idf_components: + - freertos # Test escape hatch (freertos is always included anyway) + disable_debug_stubs: true + disable_ocd_aware: true + disable_usb_serial_jtag_secondary: true + disable_dev_null_vfs: true + disable_mbedtls_peer_cert: true + disable_mbedtls_pkcs7: true + disable_regi2c_in_iram: true + disable_fatfs: true wifi: ssid: MySSID diff --git a/tests/components/esp32/test.esp32-p4-idf.yaml b/tests/components/esp32/test.esp32-p4-idf.yaml index 00a4ceec27..d67787b3d5 100644 --- a/tests/components/esp32/test.esp32-p4-idf.yaml +++ b/tests/components/esp32/test.esp32-p4-idf.yaml @@ -10,6 +10,14 @@ esp32: ref: 2.7.0 advanced: enable_idf_experimental_features: yes + disable_debug_stubs: true + disable_ocd_aware: true + disable_usb_serial_jtag_secondary: true + disable_dev_null_vfs: true + disable_mbedtls_peer_cert: true + disable_mbedtls_pkcs7: true + disable_regi2c_in_iram: true + disable_fatfs: true ota: platform: esphome diff --git a/tests/components/esp32/test.esp32-s3-idf.yaml b/tests/components/esp32/test.esp32-s3-idf.yaml index 4ae5e6b999..7a3bbe55b3 100644 --- a/tests/components/esp32/test.esp32-s3-idf.yaml +++ b/tests/components/esp32/test.esp32-s3-idf.yaml @@ -5,6 +5,14 @@ esp32: advanced: execute_from_psram: true disable_libc_locks_in_iram: true # Test default RAM optimization enabled + disable_debug_stubs: true + disable_ocd_aware: true + disable_usb_serial_jtag_secondary: true + disable_dev_null_vfs: true + disable_mbedtls_peer_cert: true + disable_mbedtls_pkcs7: true + disable_regi2c_in_iram: true + disable_fatfs: true psram: mode: octal diff --git a/tests/components/esp32_ble/test.esp32-p4-idf.yaml b/tests/components/esp32_ble/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..4eeb7c2f18 --- /dev/null +++ b/tests/components/esp32_ble/test.esp32-p4-idf.yaml @@ -0,0 +1,8 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble: + io_capability: keyboard_only + disable_bt_logs: false diff --git a/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..b6e1845c50 --- /dev/null +++ b/tests/components/esp32_ble_beacon/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +# tx_power is not supported on ESP-Hosted platforms +esp32_ble_beacon: + type: iBeacon + uuid: 'c29ce823-e67a-4e71-bff2-abaa32e77a98' diff --git a/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..e2496dd1ce --- /dev/null +++ b/tests/components/esp32_ble_client/test.esp32-p4-idf.yaml @@ -0,0 +1,6 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +ble_client: + - mac_address: 01:02:03:04:05:06 + id: blec diff --git a/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..f202161cf3 --- /dev/null +++ b/tests/components/esp32_ble_server/test.esp32-p4-idf.yaml @@ -0,0 +1,4 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml diff --git a/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml new file mode 100644 index 0000000000..d0f1e94a97 --- /dev/null +++ b/tests/components/esp32_ble_tracker/test.esp32-p4-idf.yaml @@ -0,0 +1,7 @@ +packages: + ble: !include ../../test_build_components/common/ble/esp32-p4-idf.yaml + +<<: !include common.yaml + +esp32_ble_tracker: + max_connections: 9 diff --git a/tests/components/ethernet/common-dm9051.yaml b/tests/components/ethernet/common-dm9051.yaml index 4526e7732d..bb8c74b820 100644 --- a/tests/components/ethernet/common-dm9051.yaml +++ b/tests/components/ethernet/common-dm9051.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-dp83848.yaml b/tests/components/ethernet/common-dp83848.yaml index f9069c5fb9..809613c79d 100644 --- a/tests/components/ethernet/common-dp83848.yaml +++ b/tests/components/ethernet/common-dp83848.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ip101.yaml b/tests/components/ethernet/common-ip101.yaml index cea7a5cc35..41716a7850 100644 --- a/tests/components/ethernet/common-ip101.yaml +++ b/tests/components/ethernet/common-ip101.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-jl1101.yaml b/tests/components/ethernet/common-jl1101.yaml index 7b0a2dfdc4..d70a576c81 100644 --- a/tests/components/ethernet/common-jl1101.yaml +++ b/tests/components/ethernet/common-jl1101.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ksz8081.yaml b/tests/components/ethernet/common-ksz8081.yaml index 65541832c2..e2add8d370 100644 --- a/tests/components/ethernet/common-ksz8081.yaml +++ b/tests/components/ethernet/common-ksz8081.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-ksz8081rna.yaml b/tests/components/ethernet/common-ksz8081rna.yaml index f04cba15b2..1bb404f720 100644 --- a/tests/components/ethernet/common-ksz8081rna.yaml +++ b/tests/components/ethernet/common-ksz8081rna.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-lan8670.yaml b/tests/components/ethernet/common-lan8670.yaml index fb751ebd23..ae4953974c 100644 --- a/tests/components/ethernet/common-lan8670.yaml +++ b/tests/components/ethernet/common-lan8670.yaml @@ -12,3 +12,7 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-lan8720.yaml b/tests/components/ethernet/common-lan8720.yaml index 838d57df28..742800fdf4 100644 --- a/tests/components/ethernet/common-lan8720.yaml +++ b/tests/components/ethernet/common-lan8720.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-openeth.yaml b/tests/components/ethernet/common-openeth.yaml index fbb7579598..26595dbc52 100644 --- a/tests/components/ethernet/common-openeth.yaml +++ b/tests/components/ethernet/common-openeth.yaml @@ -1,2 +1,6 @@ ethernet: type: OPENETH + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-rtl8201.yaml b/tests/components/ethernet/common-rtl8201.yaml index 0e7cbe73c6..d5a60f6e98 100644 --- a/tests/components/ethernet/common-rtl8201.yaml +++ b/tests/components/ethernet/common-rtl8201.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/ethernet/common-w5500.yaml b/tests/components/ethernet/common-w5500.yaml index b3e96f000d..1f8b8650dd 100644 --- a/tests/components/ethernet/common-w5500.yaml +++ b/tests/components/ethernet/common-w5500.yaml @@ -13,3 +13,7 @@ ethernet: subnet: 255.255.255.0 domain: .local mac_address: "02:AA:BB:CC:DD:01" + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!" diff --git a/tests/components/i2s_audio/test.esp32-ard.yaml b/tests/components/i2s_audio/test.esp32-ard.yaml new file mode 100644 index 0000000000..4276b4f922 --- /dev/null +++ b/tests/components/i2s_audio/test.esp32-ard.yaml @@ -0,0 +1,16 @@ +substitutions: + i2s_bclk_pin: GPIO15 + i2s_lrclk_pin: GPIO4 + i2s_mclk_pin: GPIO5 + +<<: !include common.yaml + +wifi: + ssid: test + password: test1234 + +media_player: + - platform: i2s_audio + name: Test Media Player + dac_type: internal + mode: stereo diff --git a/tests/components/key_collector/common.yaml b/tests/components/key_collector/common.yaml index 12e541c865..43a0478a18 100644 --- a/tests/components/key_collector/common.yaml +++ b/tests/components/key_collector/common.yaml @@ -18,14 +18,23 @@ key_collector: - logger.log: format: "input progress: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - progress: '%s'" + args: ['x.c_str()'] on_result: - logger.log: format: "input result: '%s', started by '%c', ended by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)", "(end == 0 ? '~' : end)"] + - logger.log: + format: "second listener - result: '%s'" + args: ['x.c_str()'] on_timeout: - logger.log: format: "input timeout: '%s', started by '%c'" args: ['x.c_str()', "(start == 0 ? '~' : start)"] + - logger.log: + format: "second listener - timeout: '%s'" + args: ['x.c_str()'] enable_on_boot: false button: @@ -34,3 +43,8 @@ button: on_press: - key_collector.enable: - key_collector.disable: + +text_sensor: + - platform: key_collector + id: collected_keys + source_id: reader diff --git a/tests/components/mipi_spi/test.esp8266-ard.yaml b/tests/components/mipi_spi/test.esp8266-ard.yaml new file mode 100644 index 0000000000..ef6197d852 --- /dev/null +++ b/tests/components/mipi_spi/test.esp8266-ard.yaml @@ -0,0 +1,10 @@ +substitutions: + dc_pin: GPIO15 + cs_pin: GPIO5 + enable_pin: GPIO4 + reset_pin: GPIO16 + +packages: + spi: !include ../../test_build_components/common/spi/esp8266-ard.yaml + +<<: !include common.yaml diff --git a/tests/components/pmsx003/common.yaml b/tests/components/pmsx003/common.yaml index 3c60995804..eaa3cbc3e9 100644 --- a/tests/components/pmsx003/common.yaml +++ b/tests/components/pmsx003/common.yaml @@ -8,11 +8,11 @@ sensor: pm_10_0: name: PM 10.0 Concentration pm_1_0_std: - name: PM 1.0 Standard Atmospher Concentration + name: PM 1.0 Standard Atmospheric Concentration pm_2_5_std: - name: PM 2.5 Standard Atmospher Concentration + name: PM 2.5 Standard Atmospheric Concentration pm_10_0_std: - name: PM 10.0 Standard Atmospher Concentration + name: PM 10.0 Standard Atmospheric Concentration pm_0_3um: name: Particulate Count >0.3um pm_0_5um: diff --git a/tests/components/template/common-base.yaml b/tests/components/template/common-base.yaml index 9dc65fbab8..d1849efaf7 100644 --- a/tests/components/template/common-base.yaml +++ b/tests/components/template/common-base.yaml @@ -245,6 +245,44 @@ cover: stop_action: - logger.log: stop_action optimistic: true + - platform: template + name: "Template Cover with Triggers" + id: template_cover_with_triggers + lambda: |- + if (id(some_binary_sensor).state) { + return COVER_OPEN; + } + return COVER_CLOSED; + open_action: + - logger.log: open_action + close_action: + - logger.log: close_action + stop_action: + - logger.log: stop_action + optimistic: true + on_open: + - logger.log: "Cover on_open (deprecated)" + on_opened: + - logger.log: "Cover fully opened" + on_closed: + - logger.log: "Cover fully closed" + on_opening: + - logger.log: "Cover started opening" + on_closing: + - logger.log: "Cover started closing" + on_idle: + - logger.log: "Cover stopped moving" + - logger.log: "Cover stopped moving" + - if: + condition: + cover.is_open: template_cover_with_triggers + then: + logger.log: Cover is open + - if: + condition: + cover.is_closed: template_cover_with_triggers + then: + logger.log: Cover is closed number: - platform: template @@ -296,6 +334,16 @@ select: // Migration guide: Store in std::string std::string stored_option(id(template_select).current_option()); ESP_LOGI("test", "Stored: %s", stored_option.c_str()); + - platform: template + id: template_select_with_action + name: "Template select with action" + options: + - option_a + - option_b + set_action: + - logger.log: + format: "Selected: %s" + args: ["x.c_str()"] lock: - platform: template diff --git a/tests/components/vbus/common.yaml b/tests/components/vbus/common.yaml index 33d9e2935d..5c771be922 100644 --- a/tests/components/vbus/common.yaml +++ b/tests/components/vbus/common.yaml @@ -4,11 +4,21 @@ binary_sensor: - platform: vbus model: deltasol_bs_plus relay1: - name: Relay 1 On + name: BS Plus Relay 1 On relay2: - name: Relay 2 On + name: BS Plus Relay 2 On sensor1_error: - name: Sensor 1 Error + name: BS Plus Sensor 1 Error + - platform: vbus + model: deltasol_bs2 + sensor1_error: + name: BS2 Sensor 1 Error + sensor2_error: + name: BS2 Sensor 2 Error + sensor3_error: + name: BS2 Sensor 3 Error + sensor4_error: + name: BS2 Sensor 4 Error - platform: vbus model: custom command: 0x100 @@ -23,14 +33,36 @@ sensor: - platform: vbus model: deltasol c temperature_1: - name: Temperature 1 + name: DeltaSol C Temperature 1 temperature_2: - name: Temperature 2 + name: DeltaSol C Temperature 2 temperature_3: - name: Temperature 3 + name: DeltaSol C Temperature 3 operating_hours_1: - name: Operating Hours 1 + name: DeltaSol C Operating Hours 1 heat_quantity: - name: Heat Quantity + name: DeltaSol C Heat Quantity time: - name: System Time + name: DeltaSol C System Time + - platform: vbus + model: deltasol_bs2 + temperature_1: + name: BS2 Temperature 1 + temperature_2: + name: BS2 Temperature 2 + temperature_3: + name: BS2 Temperature 3 + temperature_4: + name: BS2 Temperature 4 + pump_speed_1: + name: BS2 Pump Speed 1 + pump_speed_2: + name: BS2 Pump Speed 2 + operating_hours_1: + name: BS2 Operating Hours 1 + operating_hours_2: + name: BS2 Operating Hours 2 + heat_quantity: + name: BS2 Heat Quantity + version: + name: BS2 Firmware Version diff --git a/tests/components/wifi/common.yaml b/tests/components/wifi/common.yaml index 7ce74ab00d..10b68347eb 100644 --- a/tests/components/wifi/common.yaml +++ b/tests/components/wifi/common.yaml @@ -26,3 +26,7 @@ wifi: - ssid: MySSID3 password: password3 priority: 0 + on_connect: + - logger.log: "WiFi connected!" + on_disconnect: + - logger.log: "WiFi disconnected!" diff --git a/tests/components/zigbee/common.yaml b/tests/components/zigbee/common.yaml index e4dee5f74a..2af35ff148 100644 --- a/tests/components/zigbee/common.yaml +++ b/tests/components/zigbee/common.yaml @@ -6,8 +6,6 @@ binary_sensor: name: "Garage Door Open 2" - platform: template name: "Garage Door Open 3" - - platform: template - name: "Garage Door Open 4" - platform: template name: "Garage Door Internal" internal: True @@ -44,3 +42,11 @@ switch: - platform: template name: "Template Switch" optimistic: true + +number: + - platform: template + name: "Template number" + optimistic: true + min_value: 2 + max_value: 100 + step: 1 diff --git a/tests/dashboard/status/test_dns.py b/tests/dashboard/status/test_dns.py index 9ca48ba2d8..f7c4992079 100644 --- a/tests/dashboard/status/test_dns.py +++ b/tests/dashboard/status/test_dns.py @@ -3,11 +3,12 @@ from __future__ import annotations import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch +from icmplib import NameLookupError import pytest -from esphome.dashboard.dns import DNSCache +from esphome.dashboard.dns import DNSCache, _async_resolve_wrapper @pytest.fixture @@ -119,3 +120,80 @@ def test_async_resolve_not_called(dns_cache_fixture: DNSCache) -> None: result = dns_cache_fixture.get_cached_addresses("valid.com", now) assert result == ["192.168.1.10"] mock_resolve.assert_not_called() + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_ip_address() -> None: + """Test _async_resolve_wrapper returns IP address directly.""" + result = await _async_resolve_wrapper("192.168.1.10") + assert result == ["192.168.1.10"] + + result = await _async_resolve_wrapper("2001:db8::1") + assert result == ["2001:db8::1"] + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_success() -> None: + """Test _async_resolve_wrapper falls back to bare hostname for .local.""" + mock_resolve = AsyncMock() + # First call (device.local) fails, second call (device) succeeds + mock_resolve.side_effect = [ + NameLookupError("device.local"), + ["192.168.1.50"], + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + assert mock_resolve.call_count == 2 + mock_resolve.assert_any_call("device.local") + mock_resolve.assert_any_call("device") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_fallback_both_fail() -> None: + """Test _async_resolve_wrapper returns exception when both fail.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.local") + mock_resolve.side_effect = [ + original_exception, + NameLookupError("device"), + ] + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + # Should return the original exception, not the fallback exception + assert result is original_exception + assert mock_resolve.call_count == 2 + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_non_local_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback for non-.local hostnames.""" + mock_resolve = AsyncMock() + original_exception = NameLookupError("device.example.com") + mock_resolve.side_effect = original_exception + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.example.com") + + assert result is original_exception + # Should only try the original hostname, no fallback + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.example.com") + + +@pytest.mark.asyncio +async def test_async_resolve_wrapper_local_success_no_fallback() -> None: + """Test _async_resolve_wrapper doesn't fallback when .local succeeds.""" + mock_resolve = AsyncMock(return_value=["192.168.1.50"]) + + with patch("esphome.dashboard.dns.async_resolve", mock_resolve): + result = await _async_resolve_wrapper("device.local") + + assert result == ["192.168.1.50"] + # Should only try once since it succeeded + assert mock_resolve.call_count == 1 + mock_resolve.assert_called_once_with("device.local") diff --git a/tests/integration/fixtures/scheduler_numeric_id_test.yaml b/tests/integration/fixtures/scheduler_numeric_id_test.yaml index bf60f2fda9..1669f026f5 100644 --- a/tests/integration/fixtures/scheduler_numeric_id_test.yaml +++ b/tests/integration/fixtures/scheduler_numeric_id_test.yaml @@ -20,6 +20,9 @@ globals: - id: retry_counter type: int initial_value: '0' + - id: defer_counter + type: int + initial_value: '0' - id: tests_done type: bool initial_value: 'false' @@ -136,11 +139,49 @@ script: App.scheduler.cancel_retry(component1, 6002U); ESP_LOGI("test", "Cancelled numeric retry 6002"); + // Test 12: defer with numeric ID (Component method) + class TestDeferComponent : public Component { + public: + void test_defer_methods() { + // Test defer with uint32_t ID - should execute on next loop + this->defer(7001U, []() { + ESP_LOGI("test", "Component numeric defer 7001 fired"); + id(defer_counter) += 1; + }); + + // Test another defer with numeric ID + this->defer(7002U, []() { + ESP_LOGI("test", "Component numeric defer 7002 fired"); + id(defer_counter) += 1; + }); + } + }; + + static TestDeferComponent test_defer_component; + test_defer_component.test_defer_methods(); + + // Test 13: cancel_defer with numeric ID (Component method) + class TestCancelDeferComponent : public Component { + public: + void test_cancel_defer() { + // Set a defer that should be cancelled + this->defer(8001U, []() { + ESP_LOGE("test", "ERROR: Numeric defer 8001 should have been cancelled"); + }); + // Cancel it immediately + bool cancelled = this->cancel_defer(8001U); + ESP_LOGI("test", "Cancelled numeric defer 8001: %s", cancelled ? "true" : "false"); + } + }; + + static TestCancelDeferComponent test_cancel_defer_component; + test_cancel_defer_component.test_cancel_defer(); + - id: report_results then: - lambda: |- - ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d", - id(timeout_counter), id(interval_counter), id(retry_counter)); + ESP_LOGI("test", "Final results - Timeouts: %d, Intervals: %d, Retries: %d, Defers: %d", + id(timeout_counter), id(interval_counter), id(retry_counter), id(defer_counter)); sensor: - platform: template diff --git a/tests/integration/fixtures/select_stringref_trigger.yaml b/tests/integration/fixtures/select_stringref_trigger.yaml index bb1e1fd843..5858e2f529 100644 --- a/tests/integration/fixtures/select_stringref_trigger.yaml +++ b/tests/integration/fixtures/select_stringref_trigger.yaml @@ -56,7 +56,21 @@ select: std::string prefix = x.substr(0, 6); ESP_LOGI("test", "Substr prefix: %s", prefix.c_str()); - # Second select with numeric options to test ADL functions + # Second select with set_action trigger (uses TemplateSelectWithSetAction subclass) + - platform: template + name: "Action Select" + id: action_select + options: + - "Action A" + - "Action B" + set_action: + then: + # Test: set_action trigger receives StringRef + - logger.log: + format: "set_action triggered: %s" + args: ['x.c_str()'] + + # Third select with numeric options to test ADL functions - platform: template name: "Baud Rate" id: baud_select diff --git a/tests/integration/test_scheduler_numeric_id_test.py b/tests/integration/test_scheduler_numeric_id_test.py index 510256b9a4..c1958db685 100644 --- a/tests/integration/test_scheduler_numeric_id_test.py +++ b/tests/integration/test_scheduler_numeric_id_test.py @@ -19,6 +19,7 @@ async def test_scheduler_numeric_id_test( timeout_count = 0 interval_count = 0 retry_count = 0 + defer_count = 0 # Events for each test completion numeric_timeout_1001_fired = asyncio.Event() @@ -33,6 +34,9 @@ async def test_scheduler_numeric_id_test( max_id_timeout_fired = asyncio.Event() numeric_retry_done = asyncio.Event() numeric_retry_cancelled = asyncio.Event() + numeric_defer_7001_fired = asyncio.Event() + numeric_defer_7002_fired = asyncio.Event() + numeric_defer_cancelled = asyncio.Event() final_results_logged = asyncio.Event() # Track interval counts @@ -40,7 +44,7 @@ async def test_scheduler_numeric_id_test( numeric_retry_count = 0 def on_log_line(line: str) -> None: - nonlocal timeout_count, interval_count, retry_count + nonlocal timeout_count, interval_count, retry_count, defer_count nonlocal numeric_interval_count, numeric_retry_count # Strip ANSI color codes @@ -105,15 +109,27 @@ async def test_scheduler_numeric_id_test( elif "Cancelled numeric retry 6002" in clean_line: numeric_retry_cancelled.set() + # Check for numeric defer tests + elif "Component numeric defer 7001 fired" in clean_line: + numeric_defer_7001_fired.set() + + elif "Component numeric defer 7002 fired" in clean_line: + numeric_defer_7002_fired.set() + + elif "Cancelled numeric defer 8001: true" in clean_line: + numeric_defer_cancelled.set() + # Check for final results elif "Final results" in clean_line: match = re.search( - r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+)", clean_line + r"Timeouts: (\d+), Intervals: (\d+), Retries: (\d+), Defers: (\d+)", + clean_line, ) if match: timeout_count = int(match.group(1)) interval_count = int(match.group(2)) retry_count = int(match.group(3)) + defer_count = int(match.group(4)) final_results_logged.set() async with ( @@ -201,6 +217,23 @@ async def test_scheduler_numeric_id_test( "Numeric retry 6002 should have been cancelled" ) + # Wait for numeric defer tests + try: + await asyncio.wait_for(numeric_defer_7001_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7001 did not fire within 0.5 seconds") + + try: + await asyncio.wait_for(numeric_defer_7002_fired.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 7002 did not fire within 0.5 seconds") + + # Verify numeric defer was cancelled + try: + await asyncio.wait_for(numeric_defer_cancelled.wait(), timeout=0.5) + except TimeoutError: + pytest.fail("Numeric defer 8001 cancel confirmation not received") + # Wait for final results try: await asyncio.wait_for(final_results_logged.wait(), timeout=3.0) @@ -215,3 +248,4 @@ async def test_scheduler_numeric_id_test( assert retry_count >= 2, ( f"Expected at least 2 retry attempts, got {retry_count}" ) + assert defer_count >= 2, f"Expected at least 2 defer fires, got {defer_count}" diff --git a/tests/integration/test_script_queued.py b/tests/integration/test_script_queued.py index c86c289719..84c7f950b6 100644 --- a/tests/integration/test_script_queued.py +++ b/tests/integration/test_script_queued.py @@ -98,9 +98,11 @@ async def test_script_queued( if not test3_complete.done(): loop.call_later( 0.3, - lambda: test3_complete.set_result(True) - if not test3_complete.done() - else None, + lambda: ( + test3_complete.set_result(True) + if not test3_complete.done() + else None + ), ) # Test 4: Rejection diff --git a/tests/integration/test_select_stringref_trigger.py b/tests/integration/test_select_stringref_trigger.py index 7fc72a2290..5baba9c7f5 100644 --- a/tests/integration/test_select_stringref_trigger.py +++ b/tests/integration/test_select_stringref_trigger.py @@ -28,6 +28,8 @@ async def test_select_stringref_trigger( find_substr_future = loop.create_future() find_char_future = loop.create_future() substr_future = loop.create_future() + # set_action trigger (TemplateSelectWithSetAction subclass) + set_action_future = loop.create_future() # ADL functions stoi_future = loop.create_future() stol_future = loop.create_future() @@ -43,6 +45,8 @@ async def test_select_stringref_trigger( find_substr_pattern = re.compile(r"Found 'Option' in value") find_char_pattern = re.compile(r"Space at position: 6") # space at index 6 substr_pattern = re.compile(r"Substr prefix: Option") + # set_action trigger pattern (TemplateSelectWithSetAction subclass) + set_action_pattern = re.compile(r"set_action triggered: Action B") # ADL function patterns (115200 from baud rate select) stoi_pattern = re.compile(r"stoi result: 115200") stol_pattern = re.compile(r"stol result: 115200") @@ -67,6 +71,9 @@ async def test_select_stringref_trigger( find_char_future.set_result(True) if not substr_future.done() and substr_pattern.search(line): substr_future.set_result(True) + # set_action trigger + if not set_action_future.done() and set_action_pattern.search(line): + set_action_future.set_result(True) # ADL functions if not stoi_future.done() and stoi_pattern.search(line): stoi_future.set_result(True) @@ -89,22 +96,21 @@ async def test_select_stringref_trigger( # List entities to find our select entities, _ = await client.list_entities_services() - select_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Test Select"), - None, - ) + select_entity = next((e for e in entities if e.name == "Test Select"), None) assert select_entity is not None, "Test Select entity not found" - baud_entity = next( - (e for e in entities if hasattr(e, "options") and e.name == "Baud Rate"), - None, - ) + baud_entity = next((e for e in entities if e.name == "Baud Rate"), None) assert baud_entity is not None, "Baud Rate entity not found" + action_entity = next((e for e in entities if e.name == "Action Select"), None) + assert action_entity is not None, "Action Select entity not found" + # Change select to Option B - this should trigger on_value with StringRef client.select_command(select_entity.key, "Option B") # Change baud to 115200 - this tests ADL functions (stoi, stol, stof, stod) client.select_command(baud_entity.key, "115200") + # Change action select - tests set_action trigger (TemplateSelectWithSetAction) + client.select_command(action_entity.key, "Action B") # Wait for all log messages confirming StringRef operations work try: @@ -118,6 +124,7 @@ async def test_select_stringref_trigger( find_substr_future, find_char_future, substr_future, + set_action_future, stoi_future, stol_future, stof_future, @@ -135,6 +142,7 @@ async def test_select_stringref_trigger( "find_substr": find_substr_future.done(), "find_char": find_char_future.done(), "substr": substr_future.done(), + "set_action": set_action_future.done(), "stoi": stoi_future.done(), "stol": stol_future.done(), "stof": stof_future.done(), diff --git a/tests/script/test_helpers.py b/tests/script/test_helpers.py index c51273f298..7e60ba41fc 100644 --- a/tests/script/test_helpers.py +++ b/tests/script/test_helpers.py @@ -1011,8 +1011,8 @@ def test_get_all_dependencies_handles_missing_components() -> None: comp.dependencies = ["missing_comp"] comp.auto_load = [] - mock_get_component.side_effect = ( - lambda name: comp if name == "existing" else None + mock_get_component.side_effect = lambda name: ( + comp if name == "existing" else None ) result = helpers.get_all_dependencies({"existing", "nonexistent"}) diff --git a/tests/test_build_components/build_components_base.esp32-c2-idf.yaml b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml new file mode 100644 index 0000000000..59691be7aa --- /dev/null +++ b/tests/test_build_components/build_components_base.esp32-c2-idf.yaml @@ -0,0 +1,20 @@ +esphome: + name: componenttestesp32c2idf + friendly_name: $component_name + +esp32: + board: esp32-c2-devkitm-1 + framework: + type: esp-idf + # Use custom partition table with larger app partition (3MB) + # Default IDF partitions only allow 1.75MB which is too small for grouped tests + partitions: ../partitions_testing.csv + +logger: + level: VERY_VERBOSE + +packages: + component_under_test: !include + file: $component_test_file + vars: + component_test_file: $component_test_file diff --git a/tests/test_build_components/common/ble/esp32-p4-idf.yaml b/tests/test_build_components/common/ble/esp32-p4-idf.yaml new file mode 100644 index 0000000000..dce923078a --- /dev/null +++ b/tests/test_build_components/common/ble/esp32-p4-idf.yaml @@ -0,0 +1,21 @@ +# Common BLE tracker configuration for ESP32-P4 IDF tests +# ESP32-P4 requires ESP-Hosted for Bluetooth via external coprocessor +# BLE client components share this tracker infrastructure +# Each component defines its own ble_client with unique MAC address + +esp32_hosted: + active_high: true + variant: ESP32C6 + reset_pin: GPIO54 + cmd_pin: GPIO19 + clk_pin: GPIO18 + d0_pin: GPIO14 + d1_pin: GPIO15 + d2_pin: GPIO16 + d3_pin: GPIO17 + +esp32_ble_tracker: + scan_parameters: + interval: 1100ms + window: 1100ms + active: true diff --git a/tests/test_build_components/common/uart_2400/esp32-ard.yaml b/tests/test_build_components/common/uart_2400/esp32-ard.yaml new file mode 100644 index 0000000000..e0b6571104 --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp32-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 Arduino tests - 2400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 diff --git a/tests/test_build_components/common/uart_2400/esp32-idf.yaml b/tests/test_build_components/common/uart_2400/esp32-idf.yaml new file mode 100644 index 0000000000..7bded8c91d --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp32-idf.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP32 IDF tests - 2400 baud + +substitutions: + tx_pin: GPIO17 + rx_pin: GPIO16 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 diff --git a/tests/test_build_components/common/uart_2400/esp8266-ard.yaml b/tests/test_build_components/common/uart_2400/esp8266-ard.yaml new file mode 100644 index 0000000000..6c9a4a558d --- /dev/null +++ b/tests/test_build_components/common/uart_2400/esp8266-ard.yaml @@ -0,0 +1,11 @@ +# Common UART configuration for ESP8266 Arduino tests - 2400 baud + +substitutions: + tx_pin: GPIO4 + rx_pin: GPIO5 + +uart: + - id: uart_bus + tx_pin: ${tx_pin} + rx_pin: ${rx_pin} + baud_rate: 2400 diff --git a/tests/unit_tests/components/test_ch423.py b/tests/unit_tests/components/test_ch423.py new file mode 100644 index 0000000000..ac79fe48fe --- /dev/null +++ b/tests/unit_tests/components/test_ch423.py @@ -0,0 +1,58 @@ +"""Tests for ch423 component validation.""" + +from unittest.mock import patch + +from esphome import config, yaml_util +from esphome.core import CORE + + +def test_ch423_mixed_gpio_modes_fails(tmp_path, capsys): + """Test that mixing input/output on GPIO pins 0-7 fails validation.""" + test_file = tmp_path / "test.yaml" + test_file.write_text(""" +esphome: + name: test + +esp8266: + board: esp01_1m + +i2c: + sda: GPIO4 + scl: GPIO5 + +ch423: + - id: ch423_hub + +binary_sensor: + - platform: gpio + name: "CH423 Input 0" + pin: + ch423: ch423_hub + number: 0 + mode: input + +switch: + - platform: gpio + name: "CH423 Output 1" + pin: + ch423: ch423_hub + number: 1 + mode: output +""") + + parsed_yaml = yaml_util.load_yaml(test_file) + + with ( + patch.object(yaml_util, "load_yaml", return_value=parsed_yaml), + patch.object(CORE, "config_path", test_file), + ): + result = config.read_config({}) + + assert result is None, "Expected validation to fail with mixed GPIO modes" + + # Check that the error message mentions the GPIO pin restriction + captured = capsys.readouterr() + assert ( + "GPIO pins (0-7) must all be configured as input or all as output" + in captured.out + ) diff --git a/tests/unit_tests/core/test_config.py b/tests/unit_tests/core/test_config.py index ab7bdbb98c..88801a9ca0 100644 --- a/tests/unit_tests/core/test_config.py +++ b/tests/unit_tests/core/test_config.py @@ -453,11 +453,14 @@ def test_preload_core_config_no_platform(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Platform missing"): preload_core_config(config, result) @@ -477,11 +480,14 @@ def test_preload_core_config_multiple_platforms(setup_core: Path) -> None: # Mock _is_target_platform to avoid expensive component loading with patch("esphome.core.config._is_target_platform") as mock_is_platform: # Return True for known platforms - mock_is_platform.side_effect = lambda name: name in [ - "esp32", - "esp8266", - "rp2040", - ] + mock_is_platform.side_effect = lambda name: ( + name + in [ + "esp32", + "esp8266", + "rp2040", + ] + ) with pytest.raises(cv.Invalid, match="Found multiple target platform blocks"): preload_core_config(config, result) diff --git a/tests/unit_tests/test_core.py b/tests/unit_tests/test_core.py index 1fc8dab358..174b3fec85 100644 --- a/tests/unit_tests/test_core.py +++ b/tests/unit_tests/test_core.py @@ -780,3 +780,78 @@ class TestEsphomeCore: target.config = {const.CONF_ESPHOME: {"name": "test"}, "logger": {}} assert target.has_networking is False + + def test_add_library__esp32_arduino_enables_disabled_library(self, target): + """Test add_library auto-enables Arduino libraries on ESP32 Arduino builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("WiFi") + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp32_arduino_ignores_non_arduino_library(self, target): + """Test add_library doesn't enable libraries not in ARDUINO_DISABLED_LIBRARIES.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("SomeOtherLib", "1.0.0") + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "SomeOtherLib" in target.platformio_libraries + + def test_add_library__esp32_idf_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP32 IDF builds.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "esp-idf", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__esp8266_does_not_enable_arduino_library(self, target): + """Test add_library doesn't auto-enable Arduino libraries on ESP8266.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp8266", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("WiFi", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_not_called() + + assert "WiFi" in target.platformio_libraries + + def test_add_library__extracts_short_name_from_path(self, target): + """Test add_library extracts short name from library paths like owner/lib.""" + target.data[const.KEY_CORE] = { + const.KEY_TARGET_PLATFORM: "esp32", + const.KEY_TARGET_FRAMEWORK: "arduino", + } + + library = core.Library("arduino/Wire", None) + + with patch("esphome.components.esp32._enable_arduino_library") as mock_enable: + target.add_library(library) + mock_enable.assert_called_once_with("Wire") + + assert "Wire" in target.platformio_libraries diff --git a/tests/unit_tests/test_cpp_generator.py b/tests/unit_tests/test_cpp_generator.py index 2c9f760c8e..8755e6e2a1 100644 --- a/tests/unit_tests/test_cpp_generator.py +++ b/tests/unit_tests/test_cpp_generator.py @@ -347,3 +347,280 @@ class TestMockObj: assert isinstance(actual, cg.MockObj) assert actual.base == "foo.eek" assert actual.op == "." + + +class TestStatementFunction: + """Tests for the statement() function.""" + + def test_statement__expression_converted_to_statement(self): + """Test that expressions are converted to ExpressionStatement.""" + expr = cg.RawExpression("foo()") + result = cg.statement(expr) + + assert isinstance(result, cg.ExpressionStatement) + assert str(result) == "foo();" + + def test_statement__statement_unchanged(self): + """Test that statements are returned unchanged.""" + stmt = cg.RawStatement("foo()") + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "foo()" + + def test_statement__expression_statement_unchanged(self): + """Test that ExpressionStatement is returned unchanged.""" + stmt = cg.ExpressionStatement(42) + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "42;" + + def test_statement__line_comment_unchanged(self): + """Test that LineComment is returned unchanged.""" + stmt = cg.LineComment("This is a comment") + result = cg.statement(stmt) + + assert result is stmt + assert str(result) == "// This is a comment" + + +class TestLiteralFunction: + """Tests for the literal() function.""" + + def test_literal__creates_mockobj(self): + """Test that literal() creates a MockObj.""" + result = cg.literal("MY_CONSTANT") + + assert isinstance(result, cg.MockObj) + assert result.base == "MY_CONSTANT" + assert result.op == "" + + def test_literal__string_representation(self): + """Test that literal names appear unquoted in generated code.""" + result = cg.literal("nullptr") + + assert str(result) == "nullptr" + + def test_literal__can_be_used_in_expressions(self): + """Test that literals can be used as part of larger expressions.""" + null_lit = cg.literal("nullptr") + expr = cg.CallExpression(cg.RawExpression("my_func"), null_lit) + + assert str(expr) == "my_func(nullptr)" + + def test_literal__common_cpp_literals(self): + """Test common C++ literal values.""" + test_cases = [ + ("nullptr", "nullptr"), + ("true", "true"), + ("false", "false"), + ("NULL", "NULL"), + ("NAN", "NAN"), + ] + + for name, expected in test_cases: + result = cg.literal(name) + assert str(result) == expected + + +class TestLambdaConstructor: + """Tests for the Lambda class constructor in core/__init__.py.""" + + def test_lambda__from_string(self): + """Test Lambda constructor with string argument.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + + assert lambda_obj.value == "return x + 1;" + assert str(lambda_obj) == "return x + 1;" + + def test_lambda__from_expression(self): + """Test Lambda constructor with Expression argument.""" + from esphome.core import Lambda + + expr = cg.RawExpression("x + 1") + lambda_obj = Lambda(expr) + + # Expression should be converted to statement (with semicolon) + assert lambda_obj.value == "x + 1;" + + def test_lambda__from_lambda(self): + """Test Lambda constructor with another Lambda argument.""" + from esphome.core import Lambda + + original = Lambda("return x + 1;") + copy = Lambda(original) + + assert copy.value == original.value + assert copy.value == "return x + 1;" + + def test_lambda__parts_parsing(self): + """Test that Lambda correctly parses parts with id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return id(my_sensor).state;") + parts = lambda_obj.parts + + # Parts should be split by LAMBDA_PROG regex: text, id, op, text + assert len(parts) == 4 + assert parts[0] == "return " + assert parts[1] == "my_sensor" + assert parts[2] == "." + assert parts[3] == "state;" + + def test_lambda__requires_ids(self): + """Test that Lambda correctly extracts required IDs.""" + from esphome.core import ID, Lambda + + lambda_obj = Lambda("return id(sensor1).state + id(sensor2).value;") + ids = lambda_obj.requires_ids + + assert len(ids) == 2 + assert all(isinstance(id_obj, ID) for id_obj in ids) + assert ids[0].id == "sensor1" + assert ids[1].id == "sensor2" + + def test_lambda__no_ids(self): + """Test Lambda with no id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return 42;") + ids = lambda_obj.requires_ids + + assert len(ids) == 0 + + def test_lambda__comment_removal(self): + """Test that comments are removed when parsing parts.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return id(sensor).state; // Get sensor state") + parts = lambda_obj.parts + + # Comment should be replaced with space, not affect parsing + assert "my_sensor" not in str(parts) + + def test_lambda__multiline_string(self): + """Test Lambda with multiline string.""" + from esphome.core import Lambda + + code = """if (id(sensor).state > 0) { + return true; +} +return false;""" + lambda_obj = Lambda(code) + + assert lambda_obj.value == code + assert "sensor" in [id_obj.id for id_obj in lambda_obj.requires_ids] + + +@pytest.mark.asyncio +class TestProcessLambda: + """Tests for the process_lambda() async function.""" + + async def test_process_lambda__none_value(self): + """Test that None returns None.""" + result = await cg.process_lambda(None, []) + + assert result is None + + async def test_process_lambda__with_expression(self): + """Test process_lambda with Expression argument.""" + + expr = cg.RawExpression("return x + 1") + result = await cg.process_lambda(expr, [(int, "x")]) + + assert isinstance(result, cg.LambdaExpression) + assert "x + 1" in str(result) + + async def test_process_lambda__simple_lambda_no_ids(self): + """Test process_lambda with simple Lambda without id() references.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + result = await cg.process_lambda(lambda_obj, [(int, "x")]) + + assert isinstance(result, cg.LambdaExpression) + # Should have parameter + lambda_str = str(result) + assert "int32_t x" in lambda_str + assert "return x + 1;" in lambda_str + + async def test_process_lambda__with_return_type(self): + """Test process_lambda with return type specified.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x > 0;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], return_type=bool) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "-> bool" in lambda_str + + async def test_process_lambda__with_capture(self): + """Test process_lambda with capture specified.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return captured + x;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="captured") + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "[captured]" in lambda_str + + async def test_process_lambda__empty_capture(self): + """Test process_lambda with empty capture (stateless lambda).""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + 1;") + result = await cg.process_lambda(lambda_obj, [(int, "x")], capture="") + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "[]" in lambda_str + + async def test_process_lambda__no_parameters(self): + """Test process_lambda with no parameters.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return 42;") + result = await cg.process_lambda(lambda_obj, []) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + # Should have empty parameter list + assert "()" in lambda_str + + async def test_process_lambda__multiple_parameters(self): + """Test process_lambda with multiple parameters.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x + y + z;") + result = await cg.process_lambda( + lambda_obj, [(int, "x"), (float, "y"), (bool, "z")] + ) + + assert isinstance(result, cg.LambdaExpression) + lambda_str = str(result) + assert "int32_t x" in lambda_str + assert "float y" in lambda_str + assert "bool z" in lambda_str + + async def test_process_lambda__parameter_validation(self): + """Test that malformed parameters raise assertion error.""" + from esphome.core import Lambda + + lambda_obj = Lambda("return x;") + + # Test invalid parameter format (not list of tuples) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, "invalid") + + # Test invalid tuple format (not 2-element tuples) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, [(int, "x", "extra")]) + + # Test invalid tuple format (single element) + with pytest.raises(AssertionError): + await cg.process_lambda(lambda_obj, [(int,)]) diff --git a/tests/unit_tests/test_main.py b/tests/unit_tests/test_main.py index 3268f7ee87..c9aa446323 100644 --- a/tests/unit_tests/test_main.py +++ b/tests/unit_tests/test_main.py @@ -32,6 +32,7 @@ from esphome.__main__ import ( has_mqtt_ip_lookup, has_mqtt_logging, has_non_ip_address, + has_ota, has_resolvable_address, mqtt_get_ip, run_esphome, @@ -332,7 +333,9 @@ def test_choose_upload_log_host_with_mixed_hostnames_and_ips() -> None: def test_choose_upload_log_host_with_ota_list() -> None: """Test with OTA as the only item in the list.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=["OTA"], @@ -345,7 +348,7 @@ def test_choose_upload_log_host_with_ota_list() -> None: @pytest.mark.usefixtures("mock_has_mqtt_logging") def test_choose_upload_log_host_with_ota_list_mqtt_fallback() -> None: """Test with OTA list falling back to MQTT when no address.""" - setup_core(config={CONF_OTA: {}, "mqtt": {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], "mqtt": {}}) result = choose_upload_log_host( default=["OTA"], @@ -408,7 +411,9 @@ def test_choose_upload_log_host_with_serial_device_with_ports( def test_choose_upload_log_host_with_ota_device_with_ota_config() -> None: """Test OTA device when OTA is configured.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default="OTA", @@ -475,7 +480,9 @@ def test_choose_upload_log_host_with_ota_device_no_fallback() -> None: @pytest.mark.usefixtures("mock_choose_prompt") def test_choose_upload_log_host_multiple_devices() -> None: """Test with multiple devices including special identifiers.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) mock_ports = [MockSerialPort("/dev/ttyUSB0", "USB Serial")] @@ -514,7 +521,9 @@ def test_choose_upload_log_host_no_defaults_with_serial_ports( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_no_defaults_with_ota() -> None: """Test interactive mode with OTA option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) with patch( "esphome.__main__.choose_prompt", return_value="192.168.1.100" @@ -575,7 +584,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -604,7 +617,11 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( ) -> None: """Test interactive mode with all options available.""" setup_core( - config={CONF_OTA: {}, CONF_API: {}, CONF_MQTT: {CONF_BROKER: "mqtt.local"}}, + config={ + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], + CONF_API: {}, + CONF_MQTT: {CONF_BROKER: "mqtt.local"}, + }, address="192.168.1.100", ) @@ -632,7 +649,9 @@ def test_choose_upload_log_host_no_defaults_with_all_options_logging( @pytest.mark.usefixtures("mock_no_serial_ports") def test_choose_upload_log_host_check_default_matches() -> None: """Test when check_default matches an available option.""" - setup_core(config={CONF_OTA: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}, address="192.168.1.100" + ) result = choose_upload_log_host( default=None, @@ -704,7 +723,10 @@ def test_choose_upload_log_host_mixed_resolved_unresolved() -> None: def test_choose_upload_log_host_ota_both_conditions() -> None: """Test OTA device when both OTA and API are configured and enabled.""" - setup_core(config={CONF_OTA: {}, CONF_API: {}}, address="192.168.1.100") + setup_core( + config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}}, + address="192.168.1.100", + ) result = choose_upload_log_host( default="OTA", @@ -719,7 +741,7 @@ def test_choose_upload_log_host_ota_ip_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -744,7 +766,7 @@ def test_choose_upload_log_host_ota_local_all_options() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -769,7 +791,7 @@ def test_choose_upload_log_host_ota_ip_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -794,7 +816,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: """Test OTA device when both static IP, OTA, API and MQTT are configured and enabled but MDNS not.""" setup_core( config={ - CONF_OTA: {}, + CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}], CONF_API: {}, CONF_MQTT: { CONF_BROKER: "mqtt.local", @@ -817,7 +839,7 @@ def test_choose_upload_log_host_ota_local_all_options_logging() -> None: @pytest.mark.usefixtures("mock_no_mqtt_logging") def test_choose_upload_log_host_no_address_with_ota_config() -> None: """Test OTA device when OTA is configured but no address is set.""" - setup_core(config={CONF_OTA: {}}) + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) with pytest.raises( EsphomeError, match="All specified devices .* could not be resolved" @@ -1532,10 +1554,43 @@ def test_has_mqtt() -> None: assert has_mqtt() is False # Test with other components but no MQTT - setup_core(config={CONF_API: {}, CONF_OTA: {}}) + setup_core(config={CONF_API: {}, CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) assert has_mqtt() is False +def test_has_ota() -> None: + """Test has_ota function. + + The has_ota function should only return True when OTA is configured + with platform: esphome, not when only platform: http_request is configured. + This is because CLI OTA upload only works with the esphome platform. + """ + # Test with OTA esphome platform configured + setup_core(config={CONF_OTA: [{CONF_PLATFORM: CONF_ESPHOME}]}) + assert has_ota() is True + + # Test with OTA http_request platform only (should return False) + # This is the bug scenario from issue #13783 + setup_core(config={CONF_OTA: [{CONF_PLATFORM: "http_request"}]}) + assert has_ota() is False + + # Test without OTA configured + setup_core(config={}) + assert has_ota() is False + + # Test with multiple OTA platforms including esphome + setup_core( + config={ + CONF_OTA: [{CONF_PLATFORM: "http_request"}, {CONF_PLATFORM: CONF_ESPHOME}] + } + ) + assert has_ota() is True + + # Test with empty OTA list + setup_core(config={CONF_OTA: []}) + assert has_ota() is False + + def test_get_port_type() -> None: """Test get_port_type function.""" diff --git a/tests/unit_tests/test_wizard.py b/tests/unit_tests/test_wizard.py index fd53a0b0b7..eb44c1c20f 100644 --- a/tests/unit_tests/test_wizard.py +++ b/tests/unit_tests/test_wizard.py @@ -25,7 +25,6 @@ def default_config() -> dict[str, Any]: "board": "esp01_1m", "ssid": "test_ssid", "psk": "test_psk", - "password": "", } @@ -37,7 +36,7 @@ def wizard_answers() -> list[str]: "nodemcuv2", # board "SSID", # ssid "psk", # wifi password - "ota_pass", # ota password + "", # ota password (empty for no password) ] @@ -105,16 +104,35 @@ def test_config_file_should_include_ota_when_password_set( default_config: dict[str, Any], ): """ - The Over-The-Air update should be enabled when a password is set + The Over-The-Air update should be enabled when an OTA password is set """ # Given - default_config["password"] = "foo" + default_config["ota_password"] = "foo" # When config = wz.wizard_file(**default_config) # Then assert "ota:" in config + assert 'password: "foo"' in config + + +def test_config_file_should_include_api_encryption_key( + default_config: dict[str, Any], +): + """ + The API encryption key should be included when set + """ + # Given + default_config["api_encryption_key"] = "test_encryption_key_base64==" + + # When + config = wz.wizard_file(**default_config) + + # Then + assert "api:" in config + assert "encryption:" in config + assert 'key: "test_encryption_key_base64=="' in config def test_wizard_write_sets_platform( @@ -556,3 +574,61 @@ def test_wizard_write_protects_existing_config( # Then assert result is False # Should return False when file exists assert config_file.read_text() == original_content + + +def test_wizard_accepts_ota_password( + tmp_path: Path, monkeypatch: MonkeyPatch, wizard_answers: list[str] +): + """ + The wizard should pass ota_password to wizard_write when the user provides one + """ + + # Given + wizard_answers[5] = "my_ota_password" # Set OTA password + config_file = tmp_path / "test.yaml" + input_mock = MagicMock(side_effect=wizard_answers) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + wizard_write_mock = MagicMock(return_value=True) + monkeypatch.setattr(wz, "wizard_write", wizard_write_mock) + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 0 + call_kwargs = wizard_write_mock.call_args.kwargs + assert "ota_password" in call_kwargs + assert call_kwargs["ota_password"] == "my_ota_password" + + +def test_wizard_accepts_rpipico_board(tmp_path: Path, monkeypatch: MonkeyPatch): + """ + The wizard should handle rpipico board which doesn't support WiFi. + This tests the branch where api_encryption_key is None. + """ + + # Given + wizard_answers_rp2040 = [ + "test-node", # Name of the node + "RP2040", # platform + "rpipico", # board (no WiFi support) + ] + config_file = tmp_path / "test.yaml" + input_mock = MagicMock(side_effect=wizard_answers_rp2040) + monkeypatch.setattr("builtins.input", input_mock) + monkeypatch.setattr(wz, "safe_print", lambda t=None, end=None: 0) + monkeypatch.setattr(wz, "sleep", lambda _: 0) + wizard_write_mock = MagicMock(return_value=True) + monkeypatch.setattr(wz, "wizard_write", wizard_write_mock) + + # When + retval = wz.wizard(config_file) + + # Then + assert retval == 0 + call_kwargs = wizard_write_mock.call_args.kwargs + # rpipico doesn't support WiFi, so no api_encryption_key or ota_password + assert "api_encryption_key" not in call_kwargs + assert "ota_password" not in call_kwargs diff --git a/tests/unit_tests/test_writer.py b/tests/unit_tests/test_writer.py index ac05e0d31b..134b63df4a 100644 --- a/tests/unit_tests/test_writer.py +++ b/tests/unit_tests/test_writer.py @@ -466,8 +466,8 @@ def test_clean_build( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: str(platformio_cache_dir) + mock_config.get.side_effect = lambda section, option: ( + str(platformio_cache_dir) if (section, option) == ("platformio", "cache_dir") else "" ) @@ -630,8 +630,8 @@ def test_clean_build_empty_cache_dir( ) as mock_get_instance: mock_config = MagicMock() mock_get_instance.return_value = mock_config - mock_config.get.side_effect = ( - lambda section, option: " " # Whitespace only + mock_config.get.side_effect = lambda section, option: ( + " " # Whitespace only if (section, option) == ("platformio", "cache_dir") else "" ) @@ -1574,8 +1574,8 @@ def test_copy_src_tree_writes_build_info_files( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "Test comment" @@ -1649,8 +1649,8 @@ def test_copy_src_tree_detects_config_hash_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF # Different from existing mock_core.comment = "" @@ -1711,8 +1711,8 @@ def test_copy_src_tree_detects_version_change( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1761,8 +1761,8 @@ def test_copy_src_tree_handles_invalid_build_info_json( build_info_h_path.write_text("// old build_info_data.h") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1835,8 +1835,8 @@ def test_copy_src_tree_build_info_timestamp_behavior( mock_component.resources = mock_resources # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1930,8 +1930,8 @@ def test_copy_src_tree_detects_removed_source_file( existing_file.write_text("// test file") # Setup mocks - no components, so the file should be removed - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = "" @@ -1992,8 +1992,8 @@ def test_copy_src_tree_ignores_removed_generated_file( build_info_h.write_text("// old generated file") # Setup mocks - mock_core.relative_src_path.side_effect = lambda *args: src_path.joinpath(*args) - mock_core.relative_build_path.side_effect = lambda *args: build_path.joinpath(*args) + mock_core.relative_src_path.side_effect = src_path.joinpath + mock_core.relative_build_path.side_effect = build_path.joinpath mock_core.defines = [] mock_core.config_hash = 0xDEADBEEF mock_core.comment = ""