Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
5e881738da [api] Add speed_optimized proto option for hot encode paths
Add a new (speed_optimized) message option that emits
__attribute__((optimize("O2"))) on the generated encode() and
calculate_size() methods. Under -Os, GCC does not inline the small
ProtoEncode helpers (write_raw_byte, encode_varint, etc.) into the
generated methods, causing significant overhead on hot paths.

Apply to SensorStateResponse and BluetoothLERawAdvertisementsResponse
which are the highest-frequency encode paths.
2026-04-12 19:12:31 -10:00
J. Nick Koston
5a250cc74f [api] Compile noise-c and libsodium with -O2 for speed
Crypto libraries are CPU-bound and benefit significantly from speed
optimization over the default -Os. Add a post: extra_script that
appends -O2 to noise-c and libsodium build flags when API noise
encryption is enabled. GCC uses the last -O flag, so this overrides
the global -Os for these libraries only.
2026-04-12 19:03:21 -10:00
430 changed files with 4391 additions and 15946 deletions

View File

@@ -1 +1 @@
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
d48687d988ae2a94a9973226df773478a7db1d52133545f07aa05e34fc678dcf

View File

@@ -12,7 +12,7 @@
"--privileged",
"-e",
"GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass through local USB serial to the container
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],
"appPort": 6052,

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -41,36 +41,16 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
message +=
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
`this PR is on the large side `;
if (tooManyLabels && tooManyChanges) {
message +=
`(${nonTestChanges} line changes excluding tests, across ` +
`${originalLabelCount} different components/areas)`;
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
} else if (tooManyLabels) {
message +=
`(it touches ${originalLabelCount} different components/areas)`;
message += `This PR affects ${originalLabelCount} different components/areas.`;
} else {
message += `(${nonTestChanges} line changes excluding tests)`;
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
}
message += `, which makes it harder for maintainers to review.\n\n`;
message +=
`Smaller, focused PRs tend to be reviewed much faster since they ` +
`fit into the short gaps between other maintainer work; large ones ` +
`often have to wait for a rare long uninterrupted block of time. ` +
`If you can break this up into smaller pieces that can be reviewed ` +
`independently, it will almost certainly land faster overall.\n\n`;
message +=
`Before putting more time in, it's also worth popping into ` +
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
`you scope things and flag anything already in flight.\n\n`;
message +=
`For more details (including how to split the work up), see: ` +
`https://developers.esphome.io/contributing/submitting-your-work/` +
`#how-to-approach-large-submissions`;
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
messages.push(message);
}

View File

@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
# yamllint disable-line rule:line-length
@@ -159,7 +159,7 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -198,7 +198,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -231,7 +231,7 @@ jobs:
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -253,7 +253,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
@@ -387,14 +387,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
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@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -466,14 +466,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -555,14 +555,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -817,7 +817,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -841,7 +841,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -883,7 +883,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -930,7 +930,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}

View File

@@ -1,72 +0,0 @@
name: Close PR From Fork Default Branch
on:
# pull_request_target is required so we have permission to comment and close PRs from forks.
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
issues: write
jobs:
close:
name: Close PR opened from fork's default branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
&& github.event.pull_request.head.ref == github.event.repository.default_branch
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
const defaultBranch = context.payload.repository.default_branch;
const headRepo = context.payload.pull_request.head.repo.full_name;
const body = [
`Hi @${author}, thanks for opening a pull request! :tada:`,
``,
`It looks like this PR was opened from the \`${defaultBranch}\` branch of your fork (\`${headRepo}\`), which is the same name as this repository's default branch. Working directly on \`${defaultBranch}\` in your fork causes a few problems:`,
``,
`- Your fork's \`${defaultBranch}\` branch will permanently diverge from \`esphome/esphome:${defaultBranch}\`, making it hard to keep your fork up to date.`,
`- Any additional commits you push to \`${defaultBranch}\` will be added to this PR, so you can't easily work on multiple changes at once.`,
`- Pushing maintainer fixes to your branch is awkward, since it means committing directly to your fork's default branch.`,
`- It makes local collaboration painful — \`${defaultBranch}\` in a checkout becomes ambiguous between upstream and your fork, and maintainers end up with naming collisions when fetching your branch.`,
``,
`Please re-open this as a new PR from a dedicated feature branch. The usual flow looks like:`,
``,
`\`\`\`bash`,
`# Make sure your fork's ${defaultBranch} is up to date with upstream`,
`git remote add upstream https://github.com/${owner}/${repo}.git # if you haven't already`,
`git fetch upstream`,
`git checkout ${defaultBranch}`,
`git reset --hard upstream/${defaultBranch}`,
`git push --force-with-lease origin ${defaultBranch}`,
``,
`# Create a new branch for your change and cherry-pick / re-apply your commits there`,
`git checkout -b my-feature-branch upstream/${defaultBranch}`,
`# ...re-apply your changes, then:`,
`git push origin my-feature-branch`,
`\`\`\``,
``,
`Then open a new pull request from \`my-feature-branch\` into \`${owner}/${repo}:${defaultBranch}\`.`,
``,
`Closing this PR for now — sorry for the friction, and thanks again for contributing! :heart:`,
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
state: 'closed',
});

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
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@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4.35.1
with:
category: "/language:${{matrix.language}}"

View File

@@ -8,4 +8,4 @@ on:
jobs:
lock:
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
uses: esphome/workflows/.github/workflows/lock.yml@main

View File

@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.11
rev: v0.15.10
hooks:
# Run the linter.
- id: ruff
@@ -58,7 +58,6 @@ repos:
entry: python3 script/run-in-env.py pylint
language: system
types: [python]
files: ^esphome/.+\.py$
- id: clang-tidy-hash
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed

View File

@@ -56,7 +56,6 @@ esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/audio_http/* @kahrendt
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
@@ -404,7 +403,6 @@ esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
esphome/components/qwiic_pir/* @kahrendt
esphome/components/radio_frequency/* @kbx81
esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
@@ -440,11 +438,6 @@ esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sendspin/* @kahrendt
esphome/components/sendspin/media_player/* @kahrendt
esphome/components/sendspin/media_source/* @kahrendt
esphome/components/sendspin/sensor/* @kahrendt
esphome/components/sendspin/text_sensor/* @kahrendt
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
@@ -606,6 +599,6 @@ esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @luar123 @tomaszduda23
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81

View File

@@ -4,5 +4,4 @@ include requirements.txt
recursive-include esphome *.yaml
recursive-include esphome *.cpp *.h *.tcc *.c
recursive-include esphome *.py.script
recursive-include esphome *.jinja
recursive-include esphome LICENSE.txt

View File

@@ -39,7 +39,6 @@ from esphome.const import (
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
@@ -72,7 +71,6 @@ from esphome.util import (
run_external_process,
safe_print,
)
from esphome.zeroconf import discover_mdns_devices
_LOGGER = logging.getLogger(__name__)
@@ -206,64 +204,6 @@ def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
return [address]
def _populate_mdns_cache(hosts_to_addresses: dict[str, list[str]]) -> None:
"""Store discovered ``host -> [ips]`` entries in ``CORE.address_cache``.
Ensures ``CORE.address_cache`` exists, then records each mDNS hostname so
the downstream resolution path (``resolve_ip_address``) can skip opening a
second Zeroconf client.
"""
from esphome.address_cache import AddressCache
if CORE.address_cache is None:
CORE.address_cache = AddressCache()
for host, addresses in hosts_to_addresses.items():
if addresses:
_LOGGER.debug("Caching mDNS result %s -> %s", host, addresses)
CORE.address_cache.add_mdns_addresses(host, addresses)
def _discover_mac_suffix_devices() -> list[str] | None:
"""Discover ``<name>-<mac>.local`` devices and cache their IPs.
Returns:
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
mDNS disabled, or ``CORE.address`` is already an IP). Callers should
then fall back to whatever default OTA address they normally use.
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
back to the base name: with ``name_add_mac_suffix`` enabled, the base
name by definition doesn't exist on the network.
- A non-empty sorted list of ``.local`` hostnames on success.
Populates ``CORE.address_cache`` so downstream resolution (``espota2`` or
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
already have without opening a second Zeroconf client.
"""
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
return None
_LOGGER.info("Discovering devices...")
if not (discovered := discover_mdns_devices(CORE.name)):
_LOGGER.warning(
"No devices matching '%s-<mac>.local' were discovered.", CORE.name
)
return []
_populate_mdns_cache(discovered)
return list(discovered)
def _ota_hostnames_for_default(purpose: Purpose) -> list[str]:
"""Return OTA hostname(s) for the ``--device OTA`` / default-resolve path.
When ``name_add_mac_suffix`` is enabled, returns discovered
``<name>-<mac>.local`` hostnames (possibly empty — in which case the
caller should not fall back to the base name). Otherwise falls back to
the cache-resolved ``CORE.address``.
"""
if (discovered := _discover_mac_suffix_devices()) is not None:
return discovered
return _resolve_with_cache(CORE.address, purpose)
def choose_upload_log_host(
default: list[str] | str | None,
check_default: str | None,
@@ -302,14 +242,14 @@ def choose_upload_log_host(
resolved.append("MQTT")
if has_api() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_ota_hostnames_for_default(purpose))
resolved.extend(_resolve_with_cache(CORE.address, purpose))
elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP")
if has_ota() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_ota_hostnames_for_default(purpose))
resolved.extend(_resolve_with_cache(CORE.address, purpose))
else:
resolved.append(device)
if not resolved:
@@ -341,29 +281,22 @@ def choose_upload_log_host(
elif bootsel.permission_error:
bootsel_permission_error = True
def add_ota_options() -> None:
"""Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled."""
if (discovered := _discover_mac_suffix_devices()) is not None:
# Discovery was applicable. Use whatever we found — on empty,
# intentionally skip the base-name fallback since with
# name_add_mac_suffix on, the base name doesn't exist on the net.
for host in discovered:
options.append((f"Over The Air ({host})", host))
elif has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api():
add_ota_options()
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
elif purpose == Purpose.UPLOADING and has_ota():
add_ota_options()
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
@@ -474,17 +407,7 @@ def has_resolvable_address() -> bool:
return not CORE.address.endswith(".local")
def has_name_add_mac_suffix() -> bool:
"""Check if name_add_mac_suffix is enabled in the config."""
if CORE.config is None:
return False
esphome_config = CORE.config.get(CONF_ESPHOME, {})
return esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False)
def mqtt_get_ip(
config: ConfigType, username: str, password: str, client_id: str
) -> list[str]:
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
from esphome import mqtt
return mqtt.get_esphome_device_ip(config, username, password, client_id)
@@ -497,9 +420,6 @@ def _resolve_network_devices(
This function filters the devices list to:
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
- Expand hostnames that are already in ``CORE.address_cache`` to their
cached IPs so downstream code (e.g. aioesphomeapi) doesn't open a second
Zeroconf client to resolve them
- Deduplicate addresses while preserving order
- Only resolve MQTT once even if multiple MQTT strings are present
- If MQTT resolution fails, log a warning and continue with other devices
@@ -524,29 +444,13 @@ def _resolve_network_devices(
mqtt_ips = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
# pylint can't infer mqtt_get_ip's return through its
# lazy ``from esphome import mqtt`` import, so it flags
# the genexpr below.
network_devices.extend(
addr
for addr in mqtt_ips # pylint: disable=not-an-iterable
if addr not in network_devices
)
network_devices.extend(mqtt_ips)
except EsphomeError as err:
_LOGGER.warning(
"MQTT IP discovery failed (%s), will try other devices if available",
err,
)
mqtt_resolved = True
continue
# If the hostname is already in the address cache (e.g. populated by
# mDNS discovery), substitute the cached IPs so aioesphomeapi doesn't
# open its own Zeroconf to re-resolve it.
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(device)):
network_devices.extend(
addr for addr in cached if addr not in network_devices
)
elif device not in network_devices:
# Regular network address or IP - add if not already present
network_devices.append(device)
@@ -846,15 +750,8 @@ def upload_using_esptool(
platformio_api.FlashImage(
path=idedata.firmware_bin_path, offset=firmware_offset
),
*idedata.extra_flash_images,
]
for image in idedata.extra_flash_images:
if not image.path.is_file():
_LOGGER.warning(
"Skipping missing flash image declared by platform: %s",
image.path,
)
continue
flash_images.append(image)
mcu = "esp8266"
if CORE.is_esp32:

View File

@@ -101,17 +101,6 @@ class AddressCache:
"""Check if any cache entries exist."""
return bool(self.mdns_cache or self.dns_cache)
def add_mdns_addresses(self, hostname: str, addresses: list[str]) -> None:
"""Store resolved mDNS addresses for ``hostname`` in the cache.
Callers that discover ``.local`` hosts (e.g. via mDNS browse) can use
this to avoid a second resolution round-trip during the upload path.
No-op when ``addresses`` is empty.
"""
if not addresses:
return
self.mdns_cache[normalize_hostname(hostname)] = addresses
@classmethod
def from_cli_args(
cls, mdns_args: Iterable[str], dns_args: Iterable[str]

View File

@@ -1,56 +0,0 @@
"""Helpers for running an async coroutine from sync code via a daemon thread.
``asyncio.run(coro())`` in the main thread blocks until the loop's cleanup
cycle finishes, which can add hundreds of milliseconds before the caller
receives the result. Running the loop in a daemon thread lets the caller
observe the result as soon as the coroutine completes while cleanup finishes
in the background.
"""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
import threading
from typing import Generic, TypeVar
_T = TypeVar("_T")
class AsyncThreadRunner(threading.Thread, Generic[_T]):
"""Run an async coroutine in a daemon thread and expose its result.
The runner catches all exceptions from the coroutine and stores them in
``exception`` so ``event`` is always set — this prevents callers waiting
on ``event`` from hanging forever when the coroutine crashes.
Typical usage::
runner = AsyncThreadRunner(lambda: my_coro(arg))
runner.start()
if not runner.event.wait(timeout=5.0):
... # timed out
if runner.exception is not None:
raise runner.exception
result = runner.result
"""
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
super().__init__(daemon=True)
self._coro_factory = coro_factory
self.result: _T | None = None
self.exception: BaseException | None = None
self.event = threading.Event()
async def _runner(self) -> None:
try:
self.result = await self._coro_factory()
except Exception as exc: # pylint: disable=broad-except
# Capture all exceptions so ``event`` is always set — otherwise a
# crash would hang the waiter forever.
self.exception = exc
finally:
self.event.set()
def run(self) -> None:
asyncio.run(self._runner())

View File

@@ -199,10 +199,11 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
return cv.Schema([schema])(value)
except cv.Invalid as err2:
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
raise err from None
# pylint: disable=raise-missing-from
raise err
if "Unable to find action" in str(err):
raise err2 from None
raise cv.MultipleInvalid([err, err2]) from None
raise err2
raise cv.MultipleInvalid([err, err2])
elif isinstance(value, dict):
if CONF_THEN in value:
return [schema(value)]

View File

@@ -151,8 +151,8 @@ class ConfigBundleCreator:
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = Path(CORE.config_dir).resolve()
self._config_path = Path(CORE.config_path).resolve()
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
@@ -258,36 +258,21 @@ class ConfigBundleCreator:
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
Deliberately uses a fresh re-parse and force-loads every deferred
``IncludeFile`` to include *all* potentially-reachable includes,
even branches not selected by the local substitutions. Bundles are
meant to be compiled on another system where command-line
substitution overrides may choose a different branch — e.g.
``!include network/${eth_model}/config.yaml`` must ship every
candidate so the remote build can pick any one.
Entries with unresolved substitution variables in the filename
path are skipped with a warning (they cannot be resolved without
the substitution pass).
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
# Must be a fresh parse: IncludeFile.load() caches its result in
# _content, and we discover files by listening for loader calls. On
# an already-parsed tree the cache is populated, .load() returns
# without calling the loader, the listener never fires, and the
# referenced files would be silently dropped from the bundle.
with yaml_util.track_yaml_loads() as loaded_files:
try:
data = yaml_util.load_yaml(self._config_path)
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
else:
_force_load_include_files(data)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
@@ -623,57 +608,6 @@ def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
tar.addfile(info, io.BytesIO(data))
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
resolved during the substitution pass. During bundle discovery we need
the referenced files to actually load so the ``track_yaml_loads``
listener fires for them.
``IncludeFile`` instances with unresolved substitution variables in the
filename cannot be loaded — we skip and warn about those.
"""
if _seen is None:
_seen = set()
if isinstance(obj, yaml_util.IncludeFile):
if id(obj) in _seen:
return
_seen.add(id(obj))
if obj.has_unresolved_expressions():
_LOGGER.warning(
"Bundle: cannot resolve !include %s (referenced from %s) "
"with substitutions in path",
obj.file,
obj.parent_file,
)
return
try:
loaded = obj.load()
except EsphomeError as err:
_LOGGER.warning(
"Bundle: failed to load !include %s (referenced from %s): %s",
obj.file,
obj.parent_file,
err,
)
return
_force_load_include_files(loaded, _seen)
elif isinstance(obj, dict):
if id(obj) in _seen:
return
_seen.add(id(obj))
for value in obj.values():
_force_load_include_files(value, _seen)
elif isinstance(obj, (list, tuple)):
if id(obj) in _seen:
return
_seen.add(id(obj))
for item in obj:
_force_load_include_files(item, _seen)
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):

View File

@@ -190,7 +190,7 @@ void AcDimmer::setup() {
this->zero_cross_pin_->setup();
this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_,
this->zero_cross_interrupt_type_);
gpio::INTERRUPT_FALLING_EDGE);
}
#ifdef USE_ESP8266
@@ -226,25 +226,19 @@ void AcDimmer::write_state(float state) {
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: rising");
} else if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: falling");
} else {
ESP_LOGCONFIG(TAG, " Interrupt Type: any");
}
if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse");
ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) {
ESP_LOGCONFIG(TAG, " Method: leading");
ESP_LOGCONFIG(TAG, " Method: leading");
} else {
ESP_LOGCONFIG(TAG, " Method: trailing");
ESP_LOGCONFIG(TAG, " Method: trailing");
}
LOG_FLOAT_OUTPUT(this);
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}

View File

@@ -48,7 +48,6 @@ class AcDimmer : public output::FloatOutput, public Component {
void dump_config() override;
void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; }
void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
void set_zero_cross_interrupt_type(gpio::InterruptType type) { zero_cross_interrupt_type_ = type; }
void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
void set_method(DimMethod method) { method_ = method; }
@@ -57,7 +56,6 @@ class AcDimmer : public output::FloatOutput, public Component {
InternalGPIOPin *gate_pin_;
InternalGPIOPin *zero_cross_pin_;
gpio::InterruptType zero_cross_interrupt_type_;
AcDimmerDataStore store_;
bool init_with_half_cycle_;
DimMethod method_;

View File

@@ -7,8 +7,6 @@ from esphome.core import CORE
CODEOWNERS = ["@glmnet"]
gpio_ns = cg.esphome_ns.namespace("gpio")
ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer")
AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component)
@@ -19,26 +17,15 @@ DIM_METHODS = {
"TRAILING": DimMethod.DIM_METHOD_TRAILING,
}
ZC_INTERRUPT_TYPES = {
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
}
CONF_GATE_PIN = "gate_pin"
CONF_ZERO_CROSS_PIN = "zero_cross_pin"
CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle"
CONF_ZERO_CROSS_INTERRUPT_TYPE = "zero_cross_interrupt_type"
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(AcDimmer),
cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_ZERO_CROSS_INTERRUPT_TYPE, default="FALLING"): cv.enum(
ZC_INTERRUPT_TYPES, upper=True, space="_"
),
cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum(
DIM_METHODS, upper=True, space="_"
@@ -67,6 +54,5 @@ async def to_code(config):
cg.add(var.set_gate_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
cg.add(var.set_zero_cross_pin(pin))
cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE]))
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
cg.add(var.set_method(config[CONF_METHOD]))

View File

@@ -2,11 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
)
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,
@@ -28,7 +24,6 @@ from esphome.const import (
PlatformFramework,
)
from esphome.core import CORE
from esphome.types import ConfigType
from . import (
ATTENUATION_MODES,
@@ -70,13 +65,6 @@ def validate_config(config):
return config
def _require_adc_iram(config: ConfigType) -> ConfigType:
"""Register ADC oneshot IRAM requirement during config validation."""
if CORE.is_esp32:
require_adc_oneshot_iram()
return config
ADCSensor = adc_ns.class_(
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
)
@@ -107,7 +95,6 @@ CONFIG_SCHEMA = cv.All(
)
.extend(cv.polling_component_schema("60s")),
validate_config,
_require_adc_iram,
)
CONF_ADC_CHANNEL_ID = "adc_channel_id"

View File

@@ -2,8 +2,6 @@
#include <cstdio>
#include <cstring>
#include "esphome/core/alloc_helpers.h"
namespace esphome {
namespace anova {
@@ -107,14 +105,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
}
case READ_TARGET_TEMPERATURE:
case SET_TARGET_TEMPERATURE: {
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
if (this->fahrenheit_)
this->target_temp_ = ftoc(this->target_temp_);
this->has_target_temp_ = true;
break;
}
case READ_CURRENT_TEMPERATURE: {
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
if (this->fahrenheit_)
this->current_temp_ = ftoc(this->current_temp_);
this->has_current_temp_ = true;

View File

@@ -1,5 +1,6 @@
import base64
import logging
import pathlib
from esphome import automation
from esphome.automation import Condition
@@ -291,12 +292,12 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=5, # 520KB RAM available
esp32=8, # 520KB RAM available
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=5, # Moderate RAM
rtl87xx=5, # Moderate RAM
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
host=8, # Abundant resources
ln882x=5, # Moderate RAM
ln882x=8, # Moderate RAM
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
@@ -336,7 +337,8 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
@@ -457,6 +459,10 @@ async def to_code(config: ConfigType) -> None:
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
# Compile crypto libraries with -O2 for speed instead of -Os.
# Crypto is CPU-bound and benefits significantly from speed optimization.
# GCC uses the last -O flag, so appending -O2 overrides the global -Os.
_write_crypto_optimize_script()
else:
cg.add_define("USE_API_PLAINTEXT")
@@ -464,6 +470,17 @@ async def to_code(config: ConfigType) -> None:
cg.add_global(api_ns.using)
_CRYPTO_OPTIMIZE_SCRIPT = "crypto_optimize.py"
def _write_crypto_optimize_script() -> None:
from esphome.helpers import copy_file_if_changed
script_src = pathlib.Path(__file__).parent / f"{_CRYPTO_OPTIMIZE_SCRIPT}.script"
copy_file_if_changed(script_src, CORE.relative_build_path(_CRYPTO_OPTIMIZE_SCRIPT))
cg.add_platformio_option("extra_scripts", [f"post:{_CRYPTO_OPTIMIZE_SCRIPT}"])
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})

View File

@@ -778,10 +778,9 @@ message SubscribeLogsResponse {
option (source) = SOURCE_SERVER;
option (log) = false;
option (no_delay) = false;
option (speed_optimized) = true;
LogLevel level = 1 [(force) = true];
bytes message = 3 [(force) = true];
LogLevel level = 1;
bytes message = 3;
}
// ==================== NOISE ENCRYPTION ====================
@@ -2544,50 +2543,27 @@ message ListEntitiesInfraredResponse {
message InfraredRFTransmitRawTimingsRequest {
option (id) = 136;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
option (ifdef) = "USE_IR_RF";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
uint32 modulation = 6; // RadioFrequencyModulation enum value (0 = OOK; ignored for IR entities)
}
// Event message for received infrared/RF data
message InfraredRFReceiveEvent {
option (id) = 137;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
option (ifdef) = "USE_IR_RF";
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// ==================== RADIO FREQUENCY ====================
// Lists available radio frequency entity instances
message ListEntitiesRadioFrequencyResponse {
option (id) = 148;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_RADIO_FREQUENCY";
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitmask of RadioFrequencyCapabilityFlags: bit 0 = transmitter, bit 1 = receiver
uint32 frequency_min = 9; // Minimum tunable frequency in Hz; if min == max (non-zero): fixed frequency; 0 = unspecified
uint32 frequency_max = 10; // Maximum tunable frequency in Hz; 0 = unspecified
uint32 supported_modulations = 11; // Bitmask of supported RadioFrequencyModulation values (bit N = modulation N supported)
}
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {

View File

@@ -49,9 +49,6 @@
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_RADIO_FREQUENCY
#include "esphome/components/radio_frequency/radio_frequency.h"
#endif
namespace esphome::api {
@@ -103,12 +100,6 @@ static const int CAMERA_STOP_STREAM = 5000;
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
if ((entity_var) == nullptr) \
return;
// Helper macro for multi-entity dispatch: looks up an entity by key and device_id without early return or make_call().
// Use when multiple entity types must be checked in sequence (at most one will match).
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id)
#else // No device support, use simpler macros
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
// object
@@ -124,12 +115,6 @@ static const int CAMERA_STOP_STREAM = 5000;
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \
return;
// Helper macro for multi-entity dispatch: looks up an entity by key without early return or make_call().
// Use when multiple entity types must be checked in sequence (at most one will match).
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key)
#endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
@@ -1486,36 +1471,19 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
// and dispatch to the appropriate entity type based on that field.
#ifdef USE_INFRARED
ENTITY_COMMAND_LOOKUP(infrared::Infrared, infrared, infrared);
if (infrared != nullptr) {
auto call = infrared->make_call();
call.set_carrier_frequency(msg.carrier_frequency);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_repeat_count(msg.repeat_count);
call.perform();
return;
}
#endif
#ifdef USE_RADIO_FREQUENCY
ENTITY_COMMAND_LOOKUP(radio_frequency::RadioFrequency, radio_frequency, radio_frequency);
if (radio_frequency != nullptr) {
auto call = radio_frequency->make_call();
call.set_frequency(msg.carrier_frequency);
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
call.set_repeat_count(msg.repeat_count);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.perform();
}
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
call.set_carrier_frequency(msg.carrier_frequency);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_repeat_count(msg.repeat_count);
call.perform();
#endif
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); }
#endif
@@ -1612,19 +1580,6 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint16_t APIConnection::try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size) {
auto *rf = static_cast<radio_frequency::RadioFrequency *>(entity);
ListEntitiesRadioFrequencyResponse msg;
msg.capabilities = rf->get_capability_flags();
msg.frequency_min = rf->get_traits().get_frequency_min_hz();
msg.frequency_max = rf->get_traits().get_frequency_max_hz();
msg.supported_modulations = rf->get_traits().get_supported_modulations();
return fill_and_encode_entity_info(rf, msg, conn, remaining_size);
}
#endif
#ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
@@ -2386,9 +2341,6 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item,
#ifdef USE_INFRARED
CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_RADIO_FREQUENCY
CASE_INFO_ONLY(radio_frequency, ListEntitiesRadioFrequencyResponse)
#endif
#ifdef USE_EVENT
CASE_INFO_ONLY(event, ListEntitiesEventResponse)
#endif

View File

@@ -223,7 +223,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
@@ -612,9 +612,6 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_INFRARED
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
#endif
#ifdef USE_RADIO_FREQUENCY
static uint16_t try_send_radio_frequency_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);

View File

@@ -745,9 +745,8 @@ uint32_t ListEntitiesSensorResponse::calculate_size() const {
#endif
return size;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
__attribute__((optimize("O2"))) uint8_t *SensorStateResponse::encode(
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 13, this->key);
ProtoEncode::encode_float(pos PROTO_ENCODE_DEBUG_ARG, 2, this->state);
@@ -757,9 +756,7 @@ SensorStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) c
#endif
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SensorStateResponse::calculate_size() const {
__attribute__((optimize("O2"))) uint32_t SensorStateResponse::calculate_size() const {
uint32_t size = 0;
size += 5;
size += ProtoSize::calc_float(1, this->state);
@@ -916,22 +913,16 @@ bool SubscribeLogsRequest::decode_varint(uint32_t field_id, proto_varint_value_t
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *SubscribeLogsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level), true);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 26);
ProtoEncode::encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_len_);
ProtoEncode::encode_raw(pos PROTO_ENCODE_DEBUG_ARG, this->message_ptr_, this->message_len_);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->level));
ProtoEncode::encode_bytes(pos PROTO_ENCODE_DEBUG_ARG, 3, this->message_ptr_, this->message_len_);
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
SubscribeLogsResponse::calculate_size() const {
uint32_t SubscribeLogsResponse::calculate_size() const {
uint32_t size = 0;
size += 2;
size += ProtoSize::calc_length_force(1, this->message_len_);
size += this->level ? 2 : 0;
size += ProtoSize::calc_length(1, this->message_len_);
return size;
}
#ifdef USE_API_NOISE
@@ -2338,9 +2329,8 @@ bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id,
}
return true;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint8_t *
BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
__attribute__((optimize("O2"))) uint8_t *BluetoothLERawAdvertisementsResponse::encode(
ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
@@ -2362,9 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
}
return pos;
}
__attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
uint32_t
BluetoothLERawAdvertisementsResponse::calculate_size() const {
__attribute__((optimize("O2"))) uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const {
uint32_t size = 0;
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
@@ -3861,7 +3849,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const {
return size;
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
#ifdef USE_DEVICES
@@ -3875,9 +3863,6 @@ bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto
case 4:
this->repeat_count = value;
break;
case 6:
this->modulation = value;
break;
default:
return false;
}
@@ -3931,46 +3916,6 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const {
return size;
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id);
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key);
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name);
#ifdef USE_ENTITY_ICON
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon);
#endif
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id);
#endif
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->frequency_min);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->frequency_max);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supported_modulations);
return pos;
}
uint32_t ListEntitiesRadioFrequencyResponse::calculate_size() const {
uint32_t size = 0;
size += 2 + this->object_id.size();
size += 5;
size += 2 + this->name.size();
#ifdef USE_ENTITY_ICON
size += !this->icon.empty() ? 2 + this->icon.size() : 0;
#endif
size += ProtoSize::calc_bool(1, this->disabled_by_default);
size += this->entity_category ? 2 : 0;
#ifdef USE_DEVICES
size += ProtoSize::calc_uint32(1, this->device_id);
#endif
size += ProtoSize::calc_uint32(1, this->capabilities);
size += ProtoSize::calc_uint32(1, this->frequency_min);
size += ProtoSize::calc_uint32(1, this->frequency_max);
size += ProtoSize::calc_uint32(1, this->supported_modulations);
return size;
}
#endif
#ifdef USE_SERIAL_PROXY
bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {

View File

@@ -3054,11 +3054,11 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
protected:
};
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 224;
static constexpr uint8_t ESTIMATED_SIZE = 220;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); }
#endif
@@ -3071,7 +3071,6 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
const uint8_t *timings_data_{nullptr};
uint16_t timings_length_{0};
uint16_t timings_count_{0};
uint32_t modulation{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
@@ -3102,27 +3101,6 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_RADIO_FREQUENCY
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 148;
static constexpr uint8_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_radio_frequency_response"); }
#endif
uint32_t capabilities{0};
uint32_t frequency_min{0};
uint32_t frequency_max{0};
uint32_t supported_modulations{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_SERIAL_PROXY
class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
public:

View File

@@ -2576,7 +2576,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest"));
#ifdef USE_DEVICES
@@ -2591,7 +2591,6 @@ const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const
out.append_p(ESPHOME_PSTR(" values, "));
append_uint(out, this->timings_length_);
out.append_p(ESPHOME_PSTR(" bytes]\n"));
dump_field(out, ESPHOME_PSTR("modulation"), this->modulation);
return out.c_str();
}
const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
@@ -2606,27 +2605,6 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_RADIO_FREQUENCY
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesRadioFrequencyResponse"));
dump_field(out, ESPHOME_PSTR("object_id"), this->object_id);
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("name"), this->name);
#ifdef USE_ENTITY_ICON
dump_field(out, ESPHOME_PSTR("icon"), this->icon);
#endif
dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default);
dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities);
dump_field(out, ESPHOME_PSTR("frequency_min"), this->frequency_min);
dump_field(out, ESPHOME_PSTR("frequency_max"), this->frequency_max);
dump_field(out, ESPHOME_PSTR("supported_modulations"), this->supported_modulations);
return out.c_str();
}
#endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest"));

View File

@@ -625,7 +625,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);

View File

@@ -211,7 +211,7 @@ class APIServerConnectionBase {
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif

View File

@@ -118,7 +118,7 @@ void APIServer::loop() {
this->accept_new_connections_();
}
if (this->api_connection_count_ == 0) {
if (this->clients_.empty()) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
@@ -135,15 +135,15 @@ void APIServer::loop() {
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
}
// Continue to process and clean up the clients below
}
uint8_t client_index = 0;
while (client_index < this->api_connection_count_) {
size_t client_index = 0;
while (client_index < this->clients_.size()) {
auto &client = this->clients_[client_index];
// Common case: process active client
@@ -161,7 +161,7 @@ void APIServer::loop() {
}
}
void APIServer::remove_client_(uint8_t client_index) {
void APIServer::remove_client_(size_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -179,17 +179,14 @@ void APIServer::remove_client_(uint8_t client_index) {
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
// [api_connection_count_, N) remain nullptr.
const uint8_t last_index = this->api_connection_count_ - 1;
if (client_index < last_index) {
std::swap(this->clients_[client_index], this->clients_[last_index]);
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_[last_index].reset();
this->api_connection_count_--;
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -213,8 +210,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
@@ -223,11 +220,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_[this->api_connection_count_++].reset(conn);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -240,7 +237,7 @@ void APIServer::dump_config() {
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
@@ -258,7 +255,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->active_clients()) { \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
@@ -340,7 +337,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
@@ -352,7 +349,7 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
@@ -363,12 +360,12 @@ void APIServer::on_update(update::UpdateEntity *obj) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients
for (auto &c : this->active_clients())
for (auto &c : this->clients_)
c->send_message(msg);
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
InfraredRFReceiveEvent resp{};
@@ -378,7 +375,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
resp.key = key;
resp.timings = timings;
for (auto &c : this->active_clients())
for (auto &c : this->clients_)
c->send_infrared_rf_receive_event(resp);
}
#endif
@@ -395,7 +392,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
client->send_homeassistant_action(call);
}
}
@@ -535,7 +532,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
return;
}
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
DisconnectRequest req;
c->send_message(req);
}
@@ -586,7 +583,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->active_clients()) {
for (auto &client : this->clients_) {
if (!client->flags_.remove && client->is_authenticated()) {
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
@@ -596,8 +593,8 @@ void APIServer::request_time() {
#endif
bool APIServer::is_connected_with_state_subscription() const {
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
if (this->clients_[i]->flags_.state_subscription) {
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
return true;
}
}
@@ -612,7 +609,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
@@ -621,7 +618,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
@@ -638,7 +635,7 @@ void APIServer::on_shutdown() {
this->batch_delay_ = 5;
// Send disconnect requests to all connected clients
for (auto &c : this->active_clients()) {
for (auto &c : this->clients_) {
DisconnectRequest req;
if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full),
@@ -656,7 +653,7 @@ bool APIServer::teardown() {
this->loop();
// Return true only when all clients have been torn down
return this->api_connection_count_ == 0;
return this->clients_.empty();
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES

View File

@@ -21,8 +21,6 @@
#include "esphome/components/camera/camera.h"
#endif
#include <array>
#include <memory>
#include <vector>
namespace esphome::api {
@@ -65,6 +63,7 @@ class APIServer final : public Component,
void set_batch_delay(uint16_t batch_delay);
uint16_t get_batch_delay() const { return batch_delay_; }
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
@@ -183,30 +182,13 @@ class APIServer final : public Component,
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
bool is_connected() const { return !this->clients_.empty(); }
bool is_connected_with_state_subscription() const;
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
using APIConnectionPtr = std::unique_ptr<APIConnection>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
public:
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
const APIConnectionPtr *begin() const { return this->begin_; }
const APIConnectionPtr *end() const { return this->end_; }
};
ActiveClientsView active_clients() const {
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
}
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external)
@@ -252,8 +234,8 @@ class APIServer final : public Component,
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
void __attribute__((noinline)) remove_client_(uint8_t client_index);
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
@@ -291,9 +273,8 @@ class APIServer final : public Component,
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
@@ -328,10 +309,10 @@ class APIServer final : public Component,
uint16_t port_{6053};
uint16_t batch_delay_{100};
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
uint8_t listen_backlog_{4};
uint8_t max_connections_{8};
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE

View File

@@ -93,24 +93,7 @@ async def async_run_logs(
config, raw_line, backtrace_state=backtrace_state
)
# Safe to fall back to plaintext here only for this diagnostics use
# case: the stream is one-way from device to client, and this code
# never accepts commands or acts on any message the device sends.
# An on-path attacker could still both inject fabricated log lines
# and passively read the device's log output (and any state data
# delivered when subscribe_states is enabled), so this does lose
# confidentiality as well as authentication/integrity. That tradeoff
# is acceptable for operator-visible logs, which aioesphomeapi also
# warns may come from an unverified device. Never mirror this opt-in
# for any connection that sends data to the device or uses Home
# Assistant actions.
stop = await async_run(
cli,
on_log,
name=name,
subscribe_states=subscribe_states,
allow_plaintext_fallback=True,
)
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
try:
await asyncio.Event().wait()
finally:

View File

@@ -0,0 +1,9 @@
# Compile crypto libraries with -O2 for speed instead of the default -Os.
# Crypto is CPU-bound and benefits significantly from speed optimization.
# GCC uses the last -O flag, so appending -O2 overrides the global -Os
# for these libraries only.
Import("env")
for lb in env.GetLibBuilders():
if lb.name in ("noise-c", "libsodium"):
lb.env.Append(CCFLAGS=["-O2"])

View File

@@ -79,9 +79,6 @@ LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWater
#ifdef USE_INFRARED
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_RADIO_FREQUENCY
LIST_ENTITIES_HANDLER(radio_frequency, radio_frequency::RadioFrequency, ListEntitiesRadioFrequencyResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif

View File

@@ -87,9 +87,6 @@ class ListEntitiesIterator final : public ComponentIterator {
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *entity) override;
#endif
#ifdef USE_RADIO_FREQUENCY
bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif

View File

@@ -82,9 +82,6 @@ class InitialStateIterator final : public ComponentIterator {
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *infrared) override { return true; };
#endif
#ifdef USE_RADIO_FREQUENCY
bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; };
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif

View File

@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
value = angle(min=min, max=max)(value)
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
except cv.Invalid as e:
raise cv.Invalid(f"When using angle, {e.error_message}") from e
raise cv.Invalid(f"When using angle, {e.error_message}")
def percent_to_position(value):
@@ -164,7 +164,7 @@ def has_valid_range_config():
except cv.Invalid as e:
raise cv.Invalid(
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
) from e
)
return validator

View File

@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif

View File

@@ -1,11 +1,7 @@
from dataclasses import dataclass
import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
include_builtin_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
from esphome.core import CORE
@@ -31,7 +27,6 @@ class AudioData:
flac_support: bool = False
mp3_support: bool = False
opus_support: bool = False
micro_decoder_support: bool = False
def _get_data() -> AudioData:
@@ -55,11 +50,6 @@ def request_opus_support() -> None:
_get_data().opus_support = True
def request_micro_decoder_support() -> None:
"""Request micro-decoder library support for audio decoding."""
_get_data().micro_decoder_support = True
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
CONF_MIN_CHANNELS = "min_channels"
@@ -218,19 +208,6 @@ async def to_code(config):
)
data = _get_data()
if data.micro_decoder_support:
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
if not data.flac_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
if not data.mp3_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
if not data.opus_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
# Legacy audio_decoder.cpp support defines and components
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.1.1")

View File

@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
raise cv.Invalid(
f"Unable to determine audio file type of '{path}'. "
f"Try re-encoding the file into a supported format. Details: {e}"
) from e
)
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
if file_type == "wav":

View File

@@ -1,163 +0,0 @@
#include "audio_http_media_source.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <algorithm>
namespace esphome::audio_http {
static const char *const TAG = "audio_http_media_source";
// Decoder task / buffer tuning. Kept here as constants so the header stays free of magic numbers.
static constexpr size_t DEFAULT_TRANSFER_BUFFER_SIZE = 8 * 1024; // Staging buffer between HTTP reader and decoder
static constexpr uint32_t HTTP_TIMEOUT_MS = 5000; // HTTP connect/read timeout
static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; // Max blocking time per on_audio_write() call
static constexpr uint32_t READER_WRITE_TIMEOUT_MS = 50; // Max blocking time when writing into the ring buffer
static constexpr uint8_t READER_TASK_PRIORITY = 2;
static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
static constexpr size_t READER_TASK_STACK_SIZE = 4096;
static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
static constexpr const char *const HTTP_URI_PREFIX = "http://";
static constexpr const char *const HTTPS_URI_PREFIX = "https://";
void AudioHTTPMediaSource::dump_config() {
ESP_LOGCONFIG(TAG,
"Audio HTTP Media Source:\n"
" Buffer Size: %zu bytes\n"
" Decoder Task Stack in PSRAM: %s",
this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_));
}
void AudioHTTPMediaSource::setup() {
this->disable_loop();
micro_decoder::DecoderConfig config;
config.ring_buffer_size = this->buffer_size_;
// Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
// while the decoder is still draining it, instead of oscillating between empty and full.
config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
config.http_timeout_ms = HTTP_TIMEOUT_MS;
config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS;
config.reader_priority = READER_TASK_PRIORITY;
config.decoder_priority = DECODER_TASK_PRIORITY;
config.reader_stack_size = READER_TASK_STACK_SIZE;
config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
if (this->decoder_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate decoder");
this->mark_failed();
return;
}
this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener
}
void AudioHTTPMediaSource::loop() { this->decoder_->loop(); }
bool AudioHTTPMediaSource::can_handle(const std::string &uri) const {
return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX);
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
bool AudioHTTPMediaSource::play_uri(const std::string &uri) {
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
return false;
}
// Check if source is already playing
if (this->get_state() != media_source::MediaSourceState::IDLE) {
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
return false;
}
// Validate URI starts with "http://" or "https://"
if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) {
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
return false;
}
if (this->decoder_->play_url(uri)) {
this->pause_.store(false, std::memory_order_relaxed);
this->enable_loop();
return true;
}
ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str());
return false;
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
void AudioHTTPMediaSource::handle_command(media_source::MediaSourceCommand command) {
switch (command) {
case media_source::MediaSourceCommand::STOP:
this->decoder_->stop();
break;
case media_source::MediaSourceCommand::PAUSE:
// Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
// machine from getting stuck in PAUSED when no playback is active (which would block the
// next play_uri() call via its IDLE-state precondition).
if (this->get_state() != media_source::MediaSourceState::PLAYING)
break;
// PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
// yields, which fills the ring buffer and applies back pressure that effectively pauses both
// the decoder and HTTP reader tasks.
this->set_state_(media_source::MediaSourceState::PAUSED);
this->pause_.store(true, std::memory_order_relaxed);
break;
case media_source::MediaSourceCommand::PLAY:
// Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR.
if (this->get_state() != media_source::MediaSourceState::PAUSED)
break;
this->set_state_(media_source::MediaSourceState::PLAYING);
this->pause_.store(false, std::memory_order_relaxed);
break;
default:
break;
}
}
// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
// being thread-safe with respect to its own audio writer.
size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
if (this->pause_.load(std::memory_order_relaxed)) {
vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
return 0;
}
return this->write_output(data, length, timeout_ms, this->stream_info_);
}
// Called from the decoder task before the first on_audio_write().
void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
}
// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
// loop thread and it's safe to call set_state_() directly.
void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) {
switch (state) {
case micro_decoder::DecoderState::IDLE:
this->set_state_(media_source::MediaSourceState::IDLE);
this->disable_loop();
break;
case micro_decoder::DecoderState::PLAYING:
this->set_state_(media_source::MediaSourceState::PLAYING);
break;
case micro_decoder::DecoderState::FAILED:
this->set_state_(media_source::MediaSourceState::ERROR);
break;
default:
break;
}
}
} // namespace esphome::audio_http
#endif // USE_ESP32

View File

@@ -1,59 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/components/media_source/media_source.h"
#include "esphome/core/component.h"
#include <micro_decoder/decoder_source.h>
#include <micro_decoder/types.h>
#include <atomic>
#include <memory>
#include <string>
namespace esphome::audio_http {
// Inherits from two unrelated listener-style interfaces:
// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator
// (the orchestrator calls set_listener() on us with a MediaSourceListener*).
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
// audio and state changes (we call decoder_->set_listener(this) in setup()).
// The two set_listener() methods live on different base classes and serve opposite directions.
class AudioHTTPMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
// MediaSource interface implementation
bool play_uri(const std::string &uri) override;
void handle_command(media_source::MediaSourceCommand command) override;
bool can_handle(const std::string &uri) const override;
// DecoderListener interface implementation
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override;
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override;
void on_state_change(micro_decoder::DecoderState state) override;
protected:
std::unique_ptr<micro_decoder::DecoderSource> decoder_;
audio::AudioStreamInfo stream_info_;
size_t buffer_size_{50000};
// Written from the main loop in handle_command(), read from the decoder task in
// on_audio_write(). Must be atomic to avoid a data race.
std::atomic<bool> pause_{false};
bool decoder_task_stack_in_psram_{false};
};
} // namespace esphome::audio_http
#endif // USE_ESP32

View File

@@ -1,59 +0,0 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
audio_http_ns = cg.esphome_ns.namespace("audio_http")
AudioHTTPMediaSource = audio_http_ns.class_(
"AudioHTTPMediaSource", cg.Component, media_source.MediaSource
)
def _request_micro_decoder(config: ConfigType) -> ConfigType:
audio.request_micro_decoder_support()
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
# Only require the psram component when actually enabling PSRAM stacks; validating
# the boolean first means `false` doesn't trigger the requires_component check.
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioHTTPMediaSource,
)
.extend(
{
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
min=5000, max=1000000
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
_request_micro_decoder,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_source.register_media_source(var, config)
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))

View File

@@ -332,9 +332,8 @@ def parse_multi_click_timing_str(value):
try:
state = cv.boolean(parts[0])
except cv.Invalid:
raise cv.Invalid(
f"First word must either be ON or OFF, not {parts[0]}"
) from None
# pylint: disable=raise-missing-from
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
if parts[1] != "for":
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
@@ -351,9 +350,7 @@ def parse_multi_click_timing_str(value):
try:
length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
return {CONF_STATE: state, key: str(length)}
if parts[3] != "to":
@@ -362,16 +359,12 @@ def parse_multi_click_timing_str(value):
try:
min_length = cv.positive_time_period_milliseconds(parts[2])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing minimum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
try:
max_length = cv.positive_time_period_milliseconds(parts[4])
except cv.Invalid as err:
raise cv.Invalid(
f"Multi Click Grammar Parsing maximum length failed: {err}"
) from err
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
return {
CONF_STATE: state,

View File

@@ -65,8 +65,3 @@ async def to_code(config):
@pins.PIN_SCHEMA_REGISTRY.register("bk72xx", PIN_SCHEMA)
async def pin_to_code(config):
return await libretiny.gpio.component_pin_to_code(config)
# Called by writer.py; delegates to the shared libretiny implementation.
def copy_files() -> None:
libretiny.copy_files()

View File

@@ -20,77 +20,58 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
}
void BL0906::loop() {
while (this->available())
this->flush();
if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep.
this->handle_actions_();
this->disable_loop();
if (this->current_channel_ == UINT8_MAX) {
return;
}
if (this->current_stage_ == STAGE_TEMP) {
while (this->available())
this->flush();
if (this->current_channel_ == 0) {
// Temperature
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_1) {
} else if (this->current_channel_ == 1) {
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_2) {
} else if (this->current_channel_ == 2) {
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_3) {
} else if (this->current_channel_ == 3) {
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_4) {
} else if (this->current_channel_ == 4) {
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_5) {
} else if (this->current_channel_ == 5) {
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
} else if (this->current_stage_ == STAGE_CHANNEL_6) {
} else if (this->current_channel_ == 6) {
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
} else if (this->current_stage_ == STAGE_FREQ) {
} else if (this->current_channel_ == UINT8_MAX - 2) {
// Frequency
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, this->frequency_sensor_);
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
// Voltage
this->read_data_(BL0906_V_RMS, BL0906_UREF, this->voltage_sensor_);
} else if (this->current_stage_ == STAGE_POWER) {
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 1) {
// Total power
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
// Total Energy
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
} else {
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
return;
}
this->advance_stage_();
this->current_channel_++;
this->handle_actions_();
}
void BL0906::advance_stage_() {
switch (this->current_stage_) {
case STAGE_CHANNEL_6:
this->current_stage_ = STAGE_FREQ;
break;
case STAGE_FREQ:
this->current_stage_ = STAGE_POWER;
break;
case STAGE_POWER:
// Cycle complete; sleep until the next update().
this->current_stage_ = STAGE_IDLE;
this->disable_loop();
break;
default:
this->current_stage_ = static_cast<BL0906Stage>(this->current_stage_ + 1);
break;
}
}
void BL0906::setup() {
while (this->available())
this->flush();
@@ -104,20 +85,12 @@ void BL0906::setup() {
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
// Loop stays idle until the first update() or enqueued action.
this->disable_loop();
}
void BL0906::update() {
this->current_stage_ = STAGE_TEMP;
this->enable_loop();
}
void BL0906::update() { this->current_channel_ = 0; }
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
this->action_queue_.push_back(function);
// Ensure the queue is serviced even if the read cycle has already completed.
this->enable_loop();
return this->action_queue_.size();
}

View File

@@ -12,22 +12,6 @@
namespace esphome {
namespace bl0906 {
// Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine
// jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle
// as complete and disables the loop.
enum BL0906Stage : uint8_t {
STAGE_TEMP = 0, // chip temperature
STAGE_CHANNEL_1 = 1, // per-phase current + power + energy
STAGE_CHANNEL_2 = 2,
STAGE_CHANNEL_3 = 3,
STAGE_CHANNEL_4 = 4,
STAGE_CHANNEL_5 = 5,
STAGE_CHANNEL_6 = 6,
STAGE_FREQ = UINT8_MAX - 2, // frequency + voltage
STAGE_POWER = UINT8_MAX - 1, // total power + total energy
STAGE_IDLE = UINT8_MAX, // cycle complete
};
struct DataPacket { // NOLINT(altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
@@ -95,8 +79,7 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
void bias_correction_(uint8_t address, float measurements, float correction);
BL0906Stage current_stage_{STAGE_IDLE};
void advance_stage_();
uint8_t current_channel_{0};
size_t enqueue_action_(ActionCallbackFuncPtr function);
void handle_actions_();

View File

@@ -63,7 +63,7 @@ void BM8563::read_time() {
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -6,7 +6,6 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "text_sensor"]
CONFLICTS_WITH = ["bme68x_bsec2"]
MULTI_CONF = True
CONF_BME680_BSEC_ID = "bme680_bsec_id"

View File

@@ -13,12 +13,10 @@ from esphome.const import (
)
CODEOWNERS = ["@neffs", "@kbx81"]
CONFLICTS_WITH = ["bme680_bsec"]
DOMAIN = "bme68x_bsec2"
BSEC2_LIBRARY_VERSION = "1.10.2610"
BME68x_LIBRARY_VERSION = "v1.3.40408"
CONF_ALGORITHM_OUTPUT = "algorithm_output"
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
@@ -172,9 +170,7 @@ async def to_code_base(config):
with open(path, encoding="utf-8") as f:
bsec2_iaq_config = f.read()
except Exception as e:
raise core.EsphomeError(
f"Could not open binary configuration file {path}: {e}"
) from e
raise core.EsphomeError(f"Could not open binary configuration file {path}: {e}")
# Convert retrieved BSEC2 config to an array of ints
rhs = [int(x) for x in bsec2_iaq_config.split(",")]
@@ -188,31 +184,16 @@ async def to_code_base(config):
if core.CORE.using_arduino:
cg.add_library("Wire", None)
cg.add_library("SPI", None)
if core.CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
add_idf_component(
name="boschsensortec/Bosch-BME68x-Library",
repo="https://github.com/esphome-libs/Bosch-BME68x-Library",
ref=BME68x_LIBRARY_VERSION,
)
add_idf_component(
name="boschsensortec/Bosch-BSEC2-Library",
repo="https://github.com/esphome-libs/Bosch-BSEC2-Library",
ref=BSEC2_LIBRARY_VERSION,
)
else:
cg.add_library(
"BME68x Sensor library",
None,
f"https://github.com/boschsensortec/Bosch-BME68x-Library#{BME68x_LIBRARY_VERSION}",
)
cg.add_library(
"BSEC2 Software Library",
None,
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
)
cg.add_library(
"BME68x Sensor library",
None,
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
)
cg.add_library(
"BSEC2 Software Library",
None,
f"https://github.com/boschsensortec/Bosch-BSEC2-Library.git#{BSEC2_LIBRARY_VERSION}",
)
cg.add_define("USE_BSEC2")

View File

@@ -204,27 +204,24 @@ void CSE7761Component::get_data_() {
value = this->read_(CSE7761_REG_RMSIA, 3);
this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
value = this->read_(CSE7761_REG_POWERPA, 4);
// PowerPA is two's complement signed 32-bit per datasheet
this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : static_cast<int32_t>(value);
this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value));
value = this->read_(CSE7761_REG_RMSIB, 3);
this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
value = this->read_(CSE7761_REG_POWERPB, 4);
// PowerPB is two's complement signed 32-bit per datasheet
this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : static_cast<int32_t>(value);
this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value));
// convert values and publish to sensors
float voltage = static_cast<float>(this->data_.voltage_rms) / this->coefficient_by_unit_(RMS_UC);
float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC);
if (this->voltage_sensor_ != nullptr) {
this->voltage_sensor_->publish_state(voltage);
}
for (uint8_t channel = 0; channel < 2; channel++) {
// Active power = PowerPA * PowerPAC * 1000 / 0x80000000
float active_power =
static_cast<float>(this->data_.active_power[channel]) / this->coefficient_by_unit_(POWER_PAC); // W
float amps = static_cast<float>(this->data_.current_rms[channel]) / this->coefficient_by_unit_(RMS_IAC); // A
float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W
float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A
ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps);
if (channel == 0) {
if (this->power_sensor_1_ != nullptr) {

View File

@@ -11,8 +11,10 @@ struct CSE7761DataStruct {
uint32_t frequency = 0;
uint32_t voltage_rms = 0;
uint32_t current_rms[2] = {0};
int32_t active_power[2] = {0};
uint32_t energy[2] = {0};
uint32_t active_power[2] = {0};
uint16_t coefficient[8] = {0};
uint8_t energy_update = 0;
bool ready = false;
};

View File

@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append_str(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);

View File

@@ -224,21 +224,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// Build features string
pos = buf_append_str(buf, size, pos, "|Chip: ");
pos = buf_append_str(buf, size, pos, model);
pos = buf_append_str(buf, size, pos, " Features:");
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_str(buf, size, pos, feature.name);
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
first_feature = false;
info.features &= ~feature.bit;
}
}
if (info.features != 0) {
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_printf(buf, size, pos, "Other:0x%" PRIx32, info.features);
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
}
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
@@ -271,20 +267,17 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, " Framework: Arduino");
pos = buf_append_str(buf, size, pos, "|Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
#else
ESP_LOGD(TAG, " Framework: ESP-IDF");
pos = buf_append_str(buf, size, pos, "|Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
#endif
pos = buf_append_str(buf, size, pos, "|ESP-IDF: ");
pos = buf_append_str(buf, size, pos, esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
pos = buf_append_str(buf, size, pos, "|Reset: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Wakeup: ");
pos = buf_append_str(buf, size, pos, wakeup_cause);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
return pos;
}

View File

@@ -38,12 +38,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append_str(buf, size, pos, "|Version: ");
pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10);
pos = buf_append_str(buf, size, pos, "|Reset Reason: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Chip Name: ");
pos = buf_append_str(buf, size, pos, lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);

View File

@@ -162,18 +162,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
pos = buf_append_str(buf, size, pos, supply_status);
pos = buf_append_printf(buf, size, pos, "|Main supply status: %s", supply_status);
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: ");
pos = buf_append_str(buf, size, pos, reg0_type);
pos = buf_append_str(buf, size, pos, ", ");
pos = buf_append_str(buf, size, pos, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
#ifdef USE_NRF52_REG0_VOUT
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
@@ -181,14 +177,13 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
#endif
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
}
// Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append_str(buf, size, pos, "|Regulator stage 1: ");
pos = buf_append_str(buf, size, pos, reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
// USB power state
const char *usb_state;
@@ -202,8 +197,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append_str(buf, size, pos, "|USB power state: ");
pos = buf_append_str(buf, size, pos, usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
// Power-fail comparator
bool enabled;
@@ -308,18 +302,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
pos = buf_append_str(buf, size, pos, ", VDDH: ");
pos = buf_append_str(buf, size, pos, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
} else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
}
} else {
ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
}
auto package = [](uint32_t value) {

View File

@@ -14,7 +14,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -34,7 +33,6 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_NRF52,
PlatformFramework,
)
from esphome.core import CORE
@@ -306,7 +304,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
validate_config,
)
@@ -371,8 +369,6 @@ async def to_code(config):
if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
cg.add_define("USE_DEEP_SLEEP")
@@ -417,7 +413,7 @@ async def deep_sleep_enter_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
if CONF_SLEEP_DURATION in config:
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32)
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
cg.add(var.set_sleep_duration(template_))
if CONF_UNTIL in config:

View File

@@ -59,8 +59,6 @@ void DeepSleepComponent::deep_sleep_() {
lt_deep_sleep_enter();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace esphome::deep_sleep
#endif // USE_BK72XX

View File

@@ -9,22 +9,11 @@ static const char *const TAG = "deep_sleep";
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void DeepSleepComponent::setup() {
#ifdef USE_ZEPHYR
k_sem_init(&this->wakeup_sem_, 0, 1);
#endif
global_has_deep_sleep = true;
this->schedule_sleep_();
// It can be used from another thread for waking up the device.
// It should be called as last item in setup.
global_deep_sleep.store(this);
}
void DeepSleepComponent::schedule_sleep_() {
this->next_enter_deep_sleep_ = false;
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -69,17 +58,13 @@ void DeepSleepComponent::begin_sleep(bool manual) {
if (this->sleep_duration_.has_value()) {
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
}
if (this->should_teardown_()) {
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
}
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
this->deep_sleep_();
this->schedule_sleep_();
}
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }

View File

@@ -4,7 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
@@ -15,10 +14,6 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -125,9 +120,6 @@ class DeepSleepComponent : public Component {
void prevent_deep_sleep();
void allow_deep_sleep();
#ifdef USE_ZEPHYR
void wakeup();
#endif
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -137,8 +129,6 @@ class DeepSleepComponent : public Component {
void dump_config_platform_();
bool prepare_to_sleep_();
void deep_sleep_();
void schedule_sleep_();
bool should_teardown_();
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
@@ -167,9 +157,6 @@ class DeepSleepComponent : public Component {
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};
#ifdef USE_ZEPHYR
k_sem wakeup_sem_;
#endif
};
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -256,8 +243,5 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
extern std::atomic<DeepSleepComponent *>
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace deep_sleep
} // namespace esphome

View File

@@ -165,8 +165,6 @@ void DeepSleepComponent::deep_sleep_() {
esp_deep_sleep_start();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif // USE_ESP32

View File

@@ -18,8 +18,6 @@ void DeepSleepComponent::deep_sleep_() {
ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance)
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif

View File

@@ -1,60 +0,0 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include <zephyr/sys/poweroff.h>
#include <zephyr/kernel.h>
#include <zephyr/stats/stats.h>
#include <zephyr/pm/pm.h>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
k_timeout_t sleep_duration = K_FOREVER;
if (this->sleep_duration_.has_value()) {
sleep_duration = K_USEC(*this->sleep_duration_);
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
// - The DETECT signal, optionally generated by the GPIO peripheral.
// - The ANADETECT signal, optionally generated by the LPCOMP module.
// - The SENSE signal, optionally generated by the NFC module to wake-on-field.
// - Detecting a valid USB voltage on the VBUS pin (VBUS,DETECT).
// - A reset.
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#endif
}
// It might wake up immediately if k_sem_give was called again after wake up
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
if (ret == 0) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
}
}
bool DeepSleepComponent::should_teardown_() {
if (this->sleep_duration_.has_value()) {
return false;
}
#ifdef USE_ZIGBEE
return false;
#else
return true;
#endif
}
} // namespace esphome::deep_sleep
#endif

View File

@@ -44,7 +44,7 @@ void DS1307Component::read_time() {
.year = uint16_t(ds1307_.reg.year + 10u * ds1307_.reg.year_10 + 2000),
};
rtc_time.recalc_timestamp_utc(false);
if (!rtc_time.is_valid(/*check_day_of_week=*/true, /*check_day_of_year=*/false)) {
if (!rtc_time.is_valid()) {
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
return;
}

View File

@@ -1,19 +1,8 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_RX_BUFFER_SIZE,
CONF_UART_ID,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID
CODEOWNERS = ["@glmnet", "@PolarGoose"]
@@ -32,7 +21,8 @@ CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
dsmr_ns = cg.esphome_ns.namespace("dsmr")
# Hack to prevent compile error due to ambiguity with lib namespace
dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
@@ -64,47 +54,24 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
uart_component = await cg.get_variable(config[CONF_UART_ID])
var = cg.new_Pvariable(config[CONF_ID], uart_component, config[CONF_CRC_CHECK])
cg.add(var.set_max_telegram_length(config[CONF_MAX_TELEGRAM_LENGTH]))
if CONF_DECRYPTION_KEY in config:
cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY]))
await cg.register_component(var, config)
if CONF_REQUEST_PIN in config:
request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN])
else:
request_pin = cg.nullptr
decryption_key = config.get(CONF_DECRYPTION_KEY)
if decryption_key is None:
decryption_key = cg.nullptr
var = cg.new_Pvariable(
config[CONF_ID],
uart_component,
config[CONF_CRC_CHECK],
config[CONF_MAX_TELEGRAM_LENGTH],
config[CONF_REQUEST_INTERVAL].total_milliseconds,
config[CONF_RECEIVE_TIMEOUT].total_milliseconds,
request_pin,
decryption_key,
)
await cg.register_component(var, config)
cg.add(var.set_request_pin(request_pin))
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds))
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
cg.add_library("esphome/dsmr_parser", "1.4.0")
# DSMR Parser
cg.add_library("esphome/dsmr_parser", "1.1.0")
def final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
for uart_conf in full_config["uart"]:
if uart_conf[CONF_ID] == config[CONF_UART_ID]:
rx_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE]
if rx_buffer_size < 1500:
_LOGGER.warning(
"UART '%s' rx_buffer_size should be bigger than 1500 bytes to avoid packet losses (currently %d bytes).",
config[CONF_UART_ID],
rx_buffer_size,
)
break
return config
FINAL_VALIDATE_SCHEMA = final_validate
# Crypto
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")

View File

@@ -1,183 +1,315 @@
// Ignore Zephyr. It doesn't have any encryption library.
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <dsmr_parser/util.h>
#include <AES.h>
#include <Crypto.h>
#include <GCM.h>
namespace esphome::dsmr {
static constexpr auto &TAG = "dsmr";
static void log_callback(dsmr_parser::LogLevel level, const char *fmt, va_list args) {
std::array<char, 256> buf;
vsnprintf(buf.data(), buf.size(), fmt, args);
switch (level) {
case dsmr_parser::LogLevel::ERROR:
ESP_LOGE(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::WARNING:
ESP_LOGW(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::INFO:
ESP_LOGI(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERBOSE:
ESP_LOGV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERY_VERBOSE:
ESP_LOGVV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::DEBUG:
ESP_LOGD(TAG, "%s", buf.data());
break;
}
}
static const char *const TAG = "dsmr";
void Dsmr::setup() {
dsmr_parser::Logger::set_log_function(log_callback);
this->telegram_ = new char[this->max_telegram_len_]; // NOLINT
if (this->request_pin_ != nullptr) {
this->request_pin_->setup();
}
}
void Dsmr::loop() {
if (!this->ready_to_request_data_()) {
return;
}
if (this->encryption_enabled_) {
this->receive_encrypted_telegram_();
} else {
this->receive_telegram_();
if (this->ready_to_request_data_()) {
if (this->decryption_key_.empty()) {
this->receive_telegram_();
} else {
this->receive_encrypted_telegram_();
}
}
}
bool Dsmr::ready_to_request_data_() {
if (!this->requesting_data_ && this->request_interval_reached_()) {
this->start_requesting_data_();
// When using a request pin, then wait for the next request interval.
if (this->request_pin_ != nullptr) {
if (!this->requesting_data_ && this->request_interval_reached_()) {
this->start_requesting_data_();
}
}
// Otherwise, sink serial data until next request interval.
else {
if (this->request_interval_reached_()) {
this->start_requesting_data_();
}
if (!this->requesting_data_) {
this->drain_rx_buffer_();
}
}
return this->requesting_data_;
}
bool Dsmr::request_interval_reached_() const {
bool Dsmr::request_interval_reached_() {
if (this->last_request_time_ == 0) {
return true;
}
return millis() - this->last_request_time_ > this->request_interval_;
}
bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; }
bool Dsmr::available_within_timeout_() {
// Data are available for reading on the UART bus?
// Then we can start reading right away.
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
// When we're not in the process of reading a telegram, then there is
// no need to actively wait for new data to come in.
if (!header_found_) {
return false;
}
// A telegram is being read. The smart meter might not deliver a telegram
// in one go, but instead send it in chunks with small pauses in between.
// When the UART RX buffer cannot hold a full telegram, then make sure
// that the UART read buffer does not overflow while other components
// perform their work in their loop. Do this by not returning control to
// the main loop, until the read timeout is reached.
if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) {
while (!this->receive_timeout_reached_()) {
delay(5);
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
}
}
// No new data has come in during the read timeout? Then stop reading the
// telegram and start waiting for the next one to arrive.
if (this->receive_timeout_reached_()) {
ESP_LOGW(TAG, "Timeout while reading data for telegram");
this->reset_telegram_();
}
return false;
}
void Dsmr::start_requesting_data_() {
if (this->requesting_data_) {
return;
if (!this->requesting_data_) {
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Start requesting data from P1 port");
this->request_pin_->digital_write(true);
} else {
ESP_LOGV(TAG, "Start reading data from P1 port");
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
}
ESP_LOGV(TAG, "Start reading data from P1 port");
this->flush_rx_buffer_();
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 1");
this->request_pin_->digital_write(true);
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
}
void Dsmr::stop_requesting_data_() {
if (!this->requesting_data_) {
return;
if (this->requesting_data_) {
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Stop requesting data from P1 port");
this->request_pin_->digital_write(false);
} else {
ESP_LOGV(TAG, "Stop reading data from P1 port");
}
this->drain_rx_buffer_();
this->requesting_data_ = false;
}
ESP_LOGV(TAG, "Stop reading data from P1 port");
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 0");
this->request_pin_->digital_write(false);
}
this->requesting_data_ = false;
}
void Dsmr::flush_rx_buffer_() {
ESP_LOGV(TAG, "Flush UART RX buffer");
while (!this->uart_read_chunk_().empty()) {
void Dsmr::drain_rx_buffer_() {
uint8_t buf[64];
size_t avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break;
}
}
}
void Dsmr::reset_telegram_() {
this->header_found_ = false;
this->footer_found_ = false;
this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0;
}
void Dsmr::receive_telegram_() {
for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
for (uint8_t byte : data) {
const auto telegram = this->packet_accumulator_.process_byte(byte);
if (!telegram) { // No full packet received yet
continue;
}
if (this->parse_telegram_(telegram.value())) {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram header, i.e. forward slash.
if (c == '/') {
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Check for buffer overflow.
if (this->bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Some v2.2 or v3 meters will send a new value which starts with '('
// in a new line, while the value belongs to the previous ObisId. For
// proper parsing, remove these new line characters.
if (c == '(') {
while (true) {
auto previous_char = this->telegram_[this->bytes_read_ - 1];
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--;
} else {
break;
}
}
}
// Store the byte in the buffer.
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
continue;
}
// Check for the end of the hex checksum, i.e. a newline.
if (this->footer_found_ && c == '\n') {
// Parse the telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
}
}
}
}
void Dsmr::receive_encrypted_telegram_() {
for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
for (uint8_t byte : data) {
if (this->buffer_pos_ >= this->buffer_.size()) { // Reset buffer if overflow
ESP_LOGW(TAG, "Encrypted buffer overflow, resetting");
this->buffer_pos_ = 0;
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram start byte.
if (!this->header_found_) {
if ((uint8_t) c != 0xDB) {
continue;
}
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
// Check for buffer overflow.
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Store the byte in the buffer.
this->crypt_telegram_[this->crypt_bytes_read_] = c;
this->crypt_bytes_read_++;
// Read the length of the incoming encrypted telegram.
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
// Complete header + data bytes
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
}
// Check for the end of the encrypted telegram.
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
continue;
}
ESP_LOGV(TAG, "End of encrypted telegram found");
// Decrypt the encrypted telegram.
GCM<AES128> *gcmaes128{new GCM<AES128>()};
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
// the iv is 8 bytes of the system title + 4 bytes frame counter
// system title is at byte 2 and frame counter at byte 15
for (int i = 10; i < 14; i++)
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
constexpr uint16_t iv_size{12};
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
// the ciphertext start at byte 18
&this->crypt_telegram_[18],
// cipher size
this->crypt_bytes_read_ - 17);
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
// Parse the decrypted telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
this->buffer_[this->buffer_pos_] = byte;
this->buffer_pos_++;
}
this->last_read_time_ = millis();
}
// Detect inter-frame delay. If no byte is received for more than receive_timeout, then the packet is complete.
if (millis() - this->last_read_time_ > this->receive_timeout_ && this->buffer_pos_ > 0) {
ESP_LOGV(TAG, "Encrypted telegram received (%zu bytes)", this->buffer_pos_);
const auto telegram = this->dlms_decryptor_.decrypt_inplace({this->buffer_.data(), this->buffer_pos_});
// Reset buffer position for the next packet
this->buffer_pos_ = 0;
this->last_read_time_ = 0;
if (!telegram) { // decryption failed
return;
}
// Parse and publish the telegram
this->parse_telegram_(telegram.value());
}
}
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
bool Dsmr::parse_telegram() {
MyData data;
ESP_LOGV(TAG, "Trying to parse telegram");
this->stop_requesting_data_();
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
MyData data;
if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
ESP_LOGE(TAG, "Failed to parse telegram");
const auto &res = dsmr_parser::P1Parser::parse(
data, this->telegram_, this->bytes_read_, false,
this->crc_check_); // Parse telegram according to data definition. Ignore unknown values.
if (res.err) {
// Parsing error, show it
auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_);
ESP_LOGE(TAG, "%s", err_str.c_str());
return false;
}
} else {
this->status_clear_warning();
this->publish_sensors(data);
this->status_clear_warning();
this->publish_sensors(data);
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size());
// publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(this->telegram_, this->bytes_read_);
}
return true;
}
return true;
}
void Dsmr::dump_config() {
ESP_LOGCONFIG(TAG,
"DSMR:\n"
" Max telegram length: %zu\n"
" Max telegram length: %d\n"
" Receive timeout: %.1fs",
this->buffer_.size(), this->receive_timeout_ / 1e3f);
this->max_telegram_len_, this->receive_timeout_ / 1e3f);
if (this->request_pin_ != nullptr) {
LOG_PIN(" Request Pin: ", this->request_pin_);
}
@@ -192,37 +324,30 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
}
void Dsmr::set_decryption_key_(const char *decryption_key) {
void Dsmr::set_decryption_key(const char *decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') {
this->encryption_enabled_ = false;
ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) {
delete[] this->crypt_telegram_;
this->crypt_telegram_ = nullptr;
}
return;
}
auto key = dsmr_parser::Aes128GcmDecryptionKey::from_hex(decryption_key);
if (!key) {
ESP_LOGE(TAG, "Error, decryption key has incorrect format");
this->encryption_enabled_ = false;
if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
this->decryption_key_.clear();
return;
}
ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
this->gcm_decryptor_.set_encryption_key(key.value());
this->encryption_enabled_ = true;
}
std::span<uint8_t> Dsmr::uart_read_chunk_() {
const auto avail = this->available();
if (avail == 0) {
return {};
if (this->crypt_telegram_ == nullptr) {
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT
}
size_t to_read = std::min(avail, uart_chunk_reading_buf_.size());
if (!this->read_array(uart_chunk_reading_buf_.data(), to_read)) {
return {};
}
return {uart_chunk_reading_buf_.data(), to_read};
}
} // namespace esphome::dsmr
#endif

View File

@@ -1,45 +1,30 @@
#pragma once
// Ignore Zephyr. It doesn't have any encryption library.
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/log.h"
#include <dsmr_parser/dlms_packet_decryptor.h>
#include <dsmr_parser/fields.h>
#include <dsmr_parser/packet_accumulator.h>
#include <dsmr_parser/parser.h>
#include <array>
#include <span>
#include <vector>
#if __has_include(<psa/crypto.h>)
#include <dsmr_parser/decryption/aes128gcm_tfpsa.h>
#elif __has_include(<mbedtls/gcm.h>)
#if __has_include(<mbedtls/esp_config.h>)
#include <mbedtls/esp_config.h>
#endif
#include <dsmr_parser/decryption/aes128gcm_mbedtls.h>
#elif __has_include(<bearssl/bearssl.h>)
#include <dsmr_parser/decryption/aes128gcm_bearssl.h>
#else
#error "The platform doesn't provide a compatible encryption library for dsmr_parser"
#endif
namespace esphome::dsmr {
#if __has_include(<psa/crypto.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
#elif __has_include(<mbedtls/gcm.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
#else
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
using namespace dsmr_parser::fields;
// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines
#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST)
// Neither set, set it to a dummy value to not break build
#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification)
#endif
using namespace dsmr_parser::fields;
#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST)
#define DSMR_BOTH ,
#else
#define DSMR_BOTH
#endif
#ifndef DSMR_SENSOR_LIST
#define DSMR_SENSOR_LIST(F, SEP)
@@ -49,33 +34,21 @@ using namespace dsmr_parser::fields;
#define DSMR_TEXT_SENSOR_LIST(F, SEP)
#endif
#define DSMR_IDENTITY(s) s
#define DSMR_DATA_SENSOR(s) s
#define DSMR_COMMA ,
#define DSMR_PREPEND_COMMA(...) __VA_OPT__(, ) __VA_ARGS__
#ifdef DSMR_TEXT_SENSOR_LIST_DEFINED
using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)
DSMR_PREPEND_COMMA(DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA))>;
#else
using MyData = dsmr_parser::ParsedData<DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)>;
#endif
using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)
DSMR_BOTH DSMR_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)>;
class Dsmr : public Component, public uart::UARTDevice {
public:
Dsmr(uart::UARTComponent *uart, bool crc_check, size_t max_telegram_length, uint32_t request_interval,
uint32_t receive_timeout, GPIOPin *request_pin, const char *decryption_key)
: uart::UARTDevice(uart),
request_interval_(request_interval),
receive_timeout_(receive_timeout),
request_pin_(request_pin),
buffer_(max_telegram_length),
packet_accumulator_(buffer_, crc_check) {
this->set_decryption_key_(decryption_key);
}
Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {}
void setup() override;
void loop() override;
bool parse_telegram();
void publish_sensors(MyData &data) {
#define DSMR_PUBLISH_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \
@@ -84,15 +57,20 @@ class Dsmr : public Component, public uart::UARTDevice {
#define DSMR_PUBLISH_TEXT_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \
s_##s##_->publish_state(data.s.data(), data.s.size());
s_##s##_->publish_state(data.s.c_str());
DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, )
};
void dump_config() override;
void set_decryption_key(const char *decryption_key);
// Remove before 2026.8.0
ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); }
ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); }
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; }
// Sensor setters
#define DSMR_SET_SENSOR(s) \
@@ -107,40 +85,56 @@ class Dsmr : public Component, public uart::UARTDevice {
void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; }
protected:
void set_decryption_key_(const char *decryption_key);
void receive_telegram_();
void receive_encrypted_telegram_();
void flush_rx_buffer_();
void reset_telegram_();
void drain_rx_buffer_();
bool parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram);
bool request_interval_reached_() const;
/// Wait for UART data to become available within the read timeout.
///
/// The smart meter might provide data in chunks, causing available() to
/// return 0. When we're already reading a telegram, then we don't return
/// right away (to handle further data in an upcoming loop) but wait a
/// little while using this method to see if more data are incoming.
/// By not returning, we prevent other components from taking so much
/// time that the UART RX buffer overflows and bytes of the telegram get
/// lost in the process.
bool available_within_timeout_();
// Request telegram
uint32_t request_interval_;
bool request_interval_reached_();
GPIOPin *request_pin_{nullptr};
uint32_t last_request_time_{0};
bool requesting_data_{false};
bool ready_to_request_data_();
void start_requesting_data_();
void stop_requesting_data_();
std::span<uint8_t> uart_read_chunk_();
// Config
uint32_t request_interval_;
// Read telegram
uint32_t receive_timeout_;
GPIOPin *request_pin_{nullptr};
bool receive_timeout_reached_();
size_t max_telegram_len_;
char *telegram_{nullptr};
size_t bytes_read_{0};
uint8_t *crypt_telegram_{nullptr};
size_t crypt_telegram_len_{0};
size_t crypt_bytes_read_{0};
uint32_t last_read_time_{0};
bool header_found_{false};
bool footer_found_{false};
// handled outside dsmr
text_sensor::TextSensor *s_telegram_{nullptr};
// Sensor member pointers
#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr};
DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, )
#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr};
DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, )
// State
uint32_t last_request_time_{0};
uint32_t last_read_time_{0};
bool requesting_data_{false};
bool encryption_enabled_{false};
size_t buffer_pos_{0};
std::vector<uint8_t> buffer_;
dsmr_parser::PacketAccumulator packet_accumulator_;
Aes128GcmDecryptorImpl gcm_decryptor_;
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
std::array<uint8_t, 256> uart_chunk_reading_buf_;
std::vector<uint8_t> decryption_key_{};
bool crc_check_;
};
} // namespace esphome::dsmr
#endif

View File

@@ -10,7 +10,6 @@ from esphome.const import (
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_WATER,
@@ -120,42 +119,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_imported_energy"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
@@ -548,12 +511,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_GAS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("gas_delivered_gj"): sensor.sensor_schema(
unit_of_measurement=UNIT_GIGA_JOULE,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("water_delivered"): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER,
accuracy_decimals=3,
@@ -657,12 +614,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_demand_net"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_demand_abs"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3,
@@ -777,37 +728,6 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l1"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l2"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l3"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("min_power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("period_3_for_instantaneous_values"): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -826,7 +746,6 @@ async def to_code(config):
sensors.append(f"F({key})")
if sensors:
cg.add_define("DSMR_SENSOR_LIST_DEFINED")
cg.add_define(
"DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
)

View File

@@ -15,9 +15,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log_il"): text_sensor.text_sensor_schema(),
cv.Optional("message_short"): text_sensor.text_sensor_schema(),
cv.Optional("message_long"): text_sensor.text_sensor_schema(),
cv.Optional("equipment_id"): text_sensor.text_sensor_schema(),
@@ -54,7 +52,6 @@ async def to_code(config):
text_sensors.append(f"F({key})")
if text_sensors:
cg.add_define("DSMR_TEXT_SENSOR_LIST_DEFINED")
cg.add_define(
"DSMR_TEXT_SENSOR_LIST(F, sep)",
cg.RawExpression(" sep ".join(text_sensors)),

View File

@@ -1,97 +0,0 @@
#include "epaper_spi_ssd1683.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.mono";
void EPaperSSD1683::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
// On partial update, set red RAM to inverse to remove BW ghosting
this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00});
// Set full update to 0xD7 for fast update, 0xF7 for normal
// Fast update flashes less and draws sooner but is in busy state for the same amount of time
// Manufacturer recommends not using fast update all the time, TODO expose this to the user
this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7});
this->command(0x20);
}
// Puts the display into deep sleep mode 1, only way to get out is to reset the display
// Mode 1 retains RAM while sleeping, necessary for future partial and window updates
void EPaperSSD1683::deep_sleep() {
if (this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep mode 1");
this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM
} else {
ESP_LOGV(TAG, "Deep sleep mode 2");
this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM
}
}
void EPaperSSD1683::set_window() {
// if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire
// buffer since the display RAM will not retain contents
if (!this->is_using_partial_update_()) {
this->x_low_ = 0;
this->x_high_ = this->width_;
this->y_low_ = 0;
this->y_high_ = this->height_;
}
// round x-coordinates to byte boundaries
this->x_low_ /= 8;
this->x_high_ += 7;
this->x_high_ /= 8;
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4E, {(uint8_t) this->x_low_});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
}
bool HOT EPaperSSD1683::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
if (this->send_red_) {
// round to byte boundaries
this->set_window();
}
// for monochrome, we need to send red on every refresh to prevent dirty pixels
// when doing a partial refresh
this->command(this->send_red_ ? 0x26 : 0x24);
this->current_data_index_ = this->y_low_; // actually current line
}
size_t row_length = this->x_high_ - this->x_low_;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
return false;
}
}
this->disable();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;
return false;
}
this->send_red_ = true;
return true;
}
} // namespace esphome::epaper_spi

View File

@@ -1,22 +0,0 @@
#pragma once
#include "epaper_spi_mono.h"
namespace esphome::epaper_spi {
/**
* A class for Solomon SSD1683 epaper displays.
*/
class EPaperSSD1683 : public EPaperMono {
public:
EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperMono(name, width, height, init_sequence, init_sequence_length) {}
protected:
void refresh_screen(bool partial) override;
void deep_sleep() override;
void set_window() override;
bool transfer_data() override;
};
} // namespace esphome::epaper_spi

View File

@@ -1,27 +0,0 @@
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class SSD1683(EpaperModel):
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
_width, height = self.get_dimensions(config)
return (
(0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit
(0x18, 0x80), # Select internal Temp sensor
(0x11, 0x03), # Set transform
)
ssd1683 = SSD1683("ssd1683")
goodisplay_gdey042t81 = ssd1683.extend(
"goodisplay-gdey042t81-4.2",
width=400,
height=300,
)

View File

@@ -33,7 +33,6 @@ from esphome.const import (
CONF_TYPE,
CONF_VARIANT,
CONF_VERSION,
CONF_WATCHDOG_TIMEOUT,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
@@ -129,30 +128,23 @@ ASSERTION_LEVELS = {
SIGNING_SCHEMES = {
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
"ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME",
}
# Chip variants that only support one V2 signing scheme.
# Chip variants that only support one signing scheme for Secure Boot V2.
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
# Variants not listed in either set support both RSA and ECDSA V2
# Variants not listed in either set support both RSA and ECDSA
# (e.g. C5, C6, H2, P4). New variants should be added to the
# appropriate set if they only support one scheme.
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
# when minimum_chip_revision >= 3.0, which requires special handling.
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
SIGNED_OTA_RSA_ONLY_VARIANTS = {
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
}
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
SIGNED_OTA_ECC_ONLY_VARIANTS = {
VARIANT_ESP32C2,
VARIANT_ESP32C61,
}
# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32.
# Based on SOC_SECURE_BOOT_V1 in soc_caps.h.
SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
@@ -684,7 +676,7 @@ ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(3, 3, 8),
}
ARDUINO_PLATFORM_VERSION_LOOKUP = {
cv.Version(3, 3, 8): cv.Version(55, 3, 38, "1"),
cv.Version(3, 3, 8): cv.Version(55, 3, 38),
cv.Version(3, 3, 7): cv.Version(55, 3, 37),
cv.Version(3, 3, 6): cv.Version(55, 3, 36),
cv.Version(3, 3, 5): cv.Version(55, 3, 35),
@@ -732,7 +724,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 4): cv.Version(55, 3, 38, "1"),
cv.Version(5, 5, 4): cv.Version(55, 3, 38),
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
cv.Version(5, 5, 3): cv.Version(55, 3, 37),
cv.Version(5, 5, 2): cv.Version(55, 3, 37),
@@ -752,8 +744,8 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
# The platform-espressif32 version
# - https://github.com/pioarduino/platform-espressif32/releases
PLATFORM_VERSION_LOOKUP = {
"recommended": cv.Version(55, 3, 38, "1"),
"latest": cv.Version(55, 3, 38, "1"),
"recommended": cv.Version(55, 3, 38),
"latest": cv.Version(55, 3, 38),
"dev": "https://github.com/pioarduino/platform-espressif32.git#develop",
}
@@ -999,73 +991,25 @@ def final_validate(config):
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
scheme = signed_ota[CONF_SIGNING_SCHEME]
variant = config[CONF_VARIANT]
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
scheme_path = [
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
]
# V1 ECDSA is only available on the original ESP32
if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS:
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
0
]:
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa_v1' is only supported on "
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
f"Use 'rsa3072' or 'ecdsa256' instead.",
path=scheme_path,
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=[
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
],
)
)
elif variant == VARIANT_ESP32:
# On ESP32, V2 RSA requires minimum_chip_revision >= 3.0
# Note: string comparison works here because cv.one_of constrains
# min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1").
if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"):
errs.append(
cv.Invalid(
f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} "
f"requires minimum_chip_revision: '3.0' or higher "
f"(Secure Boot V2 RSA needs chip revision 3.0+). "
f"For older chip revisions, use 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC)
elif scheme == "ecdsa256":
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa256' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with "
f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# V1 on rev 3.0+ -- suggest V2 RSA for stronger security
elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0":
_LOGGER.info(
"Using Secure Boot V1 ECDSA on %s rev %s. "
"Consider using 'rsa3072' (Secure Boot V2 RSA) for "
"stronger security on chip revision 3.0+.",
VARIANT_FRIENDLY[variant],
min_rev,
)
else:
# Non-ESP32 variants: check V2 scheme-variant compatibility
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (
conflict := scheme_variant_conflicts.get(scheme)
) and variant in conflict[0]:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=scheme_path,
)
)
if CONF_OTA not in full_config:
_LOGGER.warning(
"Signed OTA verification is enabled but no OTA component is configured. "
@@ -1114,7 +1058,6 @@ 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"
CONF_ADC_ONESHOT_IN_IRAM = "adc_oneshot_in_iram"
# VFS requirement tracking
# Components that need VFS features can call require_vfs_*() functions
@@ -1128,7 +1071,6 @@ KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
def require_vfs_select() -> None:
@@ -1226,17 +1168,6 @@ def require_fatfs() -> None:
CORE.data[KEY_ESP32][KEY_FATFS_REQUIRED] = True
def require_adc_oneshot_iram() -> None:
"""Mark that ADC oneshot IRAM safety is required by a component.
Call this from components that use the ADC oneshot driver. When flash cache is
disabled (e.g., during NVS writes by WiFi, BLE, Zigbee, or power management),
the ADC oneshot read function must be in IRAM to avoid crashes.
This sets CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM.
"""
CORE.data[KEY_ESP32][KEY_ADC_ONESHOT_IRAM_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 *)
@@ -1278,7 +1209,7 @@ FRAMEWORK_SCHEMA = cv.Schema(
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
cv.Optional(CONF_MINIMUM_CHIP_REVISION): cv.one_of(
*ESP32_CHIP_REVISIONS, string=True
*ESP32_CHIP_REVISIONS
),
cv.Optional(CONF_SRAM1_AS_IRAM, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
@@ -1337,7 +1268,6 @@ FRAMEWORK_SCHEMA = cv.Schema(
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_ADC_ONESHOT_IN_IRAM, default=False): cv.boolean,
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
}
),
@@ -1508,10 +1438,6 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
),
}
),
_detect_variant,
@@ -1879,10 +1805,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
add_idf_sdkconfig_option(
"CONFIG_ESP_TASK_WDT_TIMEOUT_S",
config[CONF_WATCHDOG_TIMEOUT].total_seconds,
)
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
@@ -2146,16 +2068,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
add_idf_sdkconfig_option("CONFIG_ESP_REGI2C_CTRL_FUNC_IN_IRAM", False)
# Place ADC oneshot control functions in IRAM for cache safety
# When flash cache is disabled (during NVS writes by WiFi, BLE, Zigbee, Thread,
# power management, etc.), ADC reads will crash if these functions are in flash.
# Components using ADC call require_adc_oneshot_iram() to force this.
if (
CORE.data[KEY_ESP32].get(KEY_ADC_ONESHOT_IRAM_REQUIRED, False)
or advanced[CONF_ADC_ONESHOT_IN_IRAM]
):
add_idf_sdkconfig_option("CONFIG_ADC_ONESHOT_CTRL_FUNC_IN_IRAM", True)
# Disable FATFS support
# Components that need FATFS (SD card, etc.) can call require_fatfs()
if CORE.data[KEY_ESP32].get(KEY_FATFS_REQUIRED, False):

View File

@@ -23,26 +23,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
void HOT yield() { vPortYield(); }
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
// safe because the two are never cross-compared: millis() values are only used for
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
// so the Scheduler is internally consistent on the esp_timer clock.
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); }
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
@@ -80,12 +61,8 @@ uint32_t arch_get_cpu_freq_hz() {
}
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StackType_t
loop_task_stack[ESPHOME_LOOP_TASK_STACK_SIZE]; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
loop_task(void *pv_params) {
void loop_task(void *pv_params) {
setup();
while (true) {
App.loop();
@@ -96,11 +73,9 @@ extern "C" void app_main() {
initArduino();
esp32::setup_preferences();
#if CONFIG_FREERTOS_UNICORE
loop_task_handle = xTaskCreateStatic(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, loop_task_stack,
&loop_task_tcb);
xTaskCreate(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle);
#else
loop_task_handle = xTaskCreateStaticPinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1,
loop_task_stack, &loop_task_tcb, 1);
xTaskCreatePinnedToCore(loop_task, "loopTask", ESPHOME_LOOP_TASK_STACK_SIZE, nullptr, 1, &loop_task_handle, 1);
#endif
}

View File

@@ -172,16 +172,10 @@ def validate_gpio_pin(pin):
exc,
)
else:
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
# Throw an exception if used for a pin that would not have resulted
# in a validation error anyway!
if ignore_pin_validation_warning:
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
"remove `ignore_pin_validation_error: true` from this pin.",
pin[CONF_NUMBER],
)
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
return pin

View File

@@ -5,7 +5,6 @@ import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
import subprocess # noqa: E402
from glob import glob # noqa: E402
@@ -26,114 +25,6 @@ def _parse_sdkconfig(sdkconfig_path):
return options
def _generate_v1_verification_key(env):
"""Generate the V1 ECDSA verification key binary and assembly source file.
Secure Boot V1 embeds the public verification key directly in the app binary
as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates
these files via custom commands, but PlatformIO's SCons bridge does not execute
them. This function replicates that logic:
1. Extracts the raw public key from the PEM signing key using espsecure.
2. Generates the .S assembly source that embeds the key bytes.
"""
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
pioenv = env.subst("$PIOENV")
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y":
return
bin_path = build_dir / "signature_verification_key.bin"
asm_path = build_dir / "signature_verification_key.bin.S"
# Determine the source of the verification key
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y":
# Extract public key from the signing key
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
if not signing_key:
return
signing_key_path = pathlib.Path(signing_key)
if not signing_key_path.exists():
print(f"Error: V1 ECDSA signing key not found: {signing_key_path}")
env.Exit(1)
return
if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime:
python_exe = env.subst("$PYTHONEXE")
result = subprocess.run(
[python_exe, "-m", "espsecure", "extract_public_key",
"--keyfile", str(signing_key_path), str(bin_path)],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"Error extracting V1 verification key: {result.stderr}")
env.Exit(1)
return
print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}")
else:
# User-provided verification key -- should already be a raw binary file
verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY")
if not verification_key:
return
verification_key_path = pathlib.Path(verification_key)
if not verification_key_path.exists():
print(f"Error: Verification key not found: {verification_key_path}")
env.Exit(1)
return
shutil.copyfile(str(verification_key_path), str(bin_path))
if not bin_path.exists():
return
# Generate the .S assembly file from the binary key data.
# Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin.
# The file is needed in both the app build dir and the bootloader build dir, since
# the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT
# is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that
# normally generate these files.
data = bin_path.read_bytes()
varname = "signature_verification_key_bin"
lines = []
lines.append(f"/* Data converted from {bin_path.name} */")
lines.append(".data")
lines.append("#if !defined (__APPLE__) && !defined (__linux__)")
lines.append(".section .rodata.embedded")
lines.append("#endif")
lines.append(f"\n.global {varname}")
lines.append(f"{varname}:")
lines.append(f"\n.global _binary_{varname}_start")
lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */")
# Format binary data as .byte lines (16 bytes per line)
for i in range(0, len(data), 16):
chunk = data[i:i + 16]
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f".byte {hex_bytes}")
lines.append(f"\n.global _binary_{varname}_end")
lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */")
lines.append(f"\n.global {varname}_length")
lines.append(f"{varname}_length:")
lines.append(f".long {len(data)}")
lines.append("")
lines.append('#if defined (__linux__)')
lines.append('.section .note.GNU-stack,"",@progbits')
lines.append("#endif")
asm_content = "\n".join(lines) + "\n"
# Write to app build dir and bootloader build dir
asm_path.write_text(asm_content)
bootloader_dir = build_dir / "bootloader"
if bootloader_dir.is_dir():
bootloader_bin = bootloader_dir / "signature_verification_key.bin"
bootloader_asm = bootloader_dir / "signature_verification_key.bin.S"
shutil.copyfile(str(bin_path), str(bootloader_bin))
bootloader_asm.write_text(asm_content)
def sign_firmware(source, target, env):
"""
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
@@ -164,12 +55,9 @@ def sign_firmware(source, target, env):
env.Exit(1)
return
# Determine espsecure signature version from the signing scheme:
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2.
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y":
sign_version = "1"
else:
sign_version = "2"
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
# so the espsecure signature version is always 2.
sign_version = "2"
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
firmware_path = build_dir / firmware_name
@@ -329,11 +217,6 @@ def esp32_copy_ota_bin(source, target, env):
print(f"Copied firmware to {new_file_name}")
# Generate V1 ECDSA verification key files before build starts.
# Workaround for PlatformIO not executing CMake custom commands that extract
# the public key and generate the .S assembly file for Secure Boot V1.
_generate_v1_verification_key(env) # noqa: F821
# Run signing first, then merge, then ota copy
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821

View File

@@ -4,6 +4,7 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <nvs_flash.h>
#include <cinttypes>
#include <cstring>
#include <vector>
@@ -11,6 +12,9 @@ namespace esphome::esp32 {
static const char *const TAG = "preferences";
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
static constexpr size_t KEY_BUFFER_SIZE = 12;
struct NVSData {
uint32_t key;
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
@@ -47,8 +51,8 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
}
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, this->key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
size_t actual_len;
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
if (err != 0) {
@@ -104,8 +108,8 @@ bool ESP32Preferences::sync() {
uint32_t last_key = 0;
for (const auto &save : s_pending_save) {
char key_str[UINT32_MAX_STR_SIZE];
uint32_to_str(key_str, save.key);
char key_str[KEY_BUFFER_SIZE];
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
if (this->is_changed_(this->nvs_handle, save, key_str)) {
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());

View File

@@ -7,7 +7,6 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2
import esphome.config_validation as cv
@@ -343,9 +342,6 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
),
cv.Optional(CONF_USE_PSRAM): cv.All(
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -602,22 +598,6 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
# When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for
# heap allocations and use dynamic (heap-based) environment memory tables
# instead of large static DRAM arrays. This frees ~40 kB of internal RAM.
# Reference: Espressif ADF Design Considerations
# https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/
# design-guide/design-considerations.html
if config.get(CONF_USE_PSRAM, False):
cg.add_define("USE_ESP32_BLE_PSRAM")
# CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is only available on ESP32
# (BTDM dual-mode controller). BLE-only SoCs (C3, S3, C2, H2) do not
# expose this Kconfig symbol; applying it there would cause a build error.
if get_esp32_variant() == const.VARIANT_ESP32:
add_idf_sdkconfig_option("CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST", True)
# CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY applies to all Bluedroid-enabled variants.
add_idf_sdkconfig_option("CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY", True)
# Register the core BLE loggers that are always needed
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)

View File

@@ -257,9 +257,11 @@ bool ESP32BLE::ble_setup_() {
if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) {
// MAC address length: 12 hex chars + null terminator
constexpr size_t mac_address_len = 13;
// MAC address suffix length (last 6 characters of 12-char MAC address string)
constexpr size_t mac_address_suffix_len = 6;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
char mac_addr[mac_address_len];
get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
@@ -665,9 +667,6 @@ void ESP32BLE::dump_config() {
" MAC address: %s\n"
" IO Capability: %s",
mac_s, io_capability_s);
#ifdef USE_ESP32_BLE_PSRAM
ESP_LOGCONFIG(TAG, " PSRAM BLE allocation: enabled");
#endif
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
const char *auth_req_mode_s = "<default>";

View File

@@ -78,14 +78,6 @@ def ota_esphome_final_validate(config):
else:
new_ota_conf.append(ota_conf)
if len(merged_ota_esphome_configs_by_port) > 1:
raise cv.Invalid(
f"Only a single port is supported for '{CONF_OTA}' "
f"'{CONF_PLATFORM}: {CONF_ESPHOME}'. Got ports "
f"{sorted(merged_ota_esphome_configs_by_port.keys())}. Consolidate "
f"onto a single port; configs sharing a port are merged automatically."
)
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
full_conf[CONF_OTA] = new_ota_conf
@@ -150,17 +142,11 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
# Compile the auth path whenever `password:` is present in YAML, even if empty.
# An empty password opts in to the auth code path so set_auth_password() can be
# called at runtime (e.g. to rotate the password from a lambda). When `password:`
# is omitted entirely, the auth path is excluded to save flash on small devices.
if CONF_PASSWORD in config:
# Password could be set to an empty string and we can assume that means no password
if config.get(CONF_PASSWORD):
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_PASSWORD")
if config[CONF_PASSWORD]:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
await cg.register_component(var, config)
await ota_to_code(var, config)

View File

@@ -15,9 +15,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_LWIP_FAST_SELECT
#include "esphome/core/lwip_fast_select.h"
#endif
#include <cerrno>
#include <cstdio>
@@ -31,17 +28,6 @@ static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
// Single-instance pointer — multi-port configs are rejected in final_validate.
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
static ESPHomeOTAComponent *global_esphome_ota_component = nullptr;
// Called from any context (LwIP TCP/IP task, RP2040 user-IRQ).
extern "C" void esphome_wake_ota_component_any_context() {
if (global_esphome_ota_component != nullptr) {
global_esphome_ota_component->enable_loop_soon_any_context();
}
}
void ESPHomeOTAComponent::setup() {
this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
if (this->server_ == nullptr) {
@@ -79,14 +65,6 @@ void ESPHomeOTAComponent::setup() {
this->server_failed_(LOG_STR("listen"));
return;
}
// loop() self-disables on its first idle tick; no explicit disable_loop() needed here.
global_esphome_ota_component = this;
#ifdef USE_LWIP_FAST_SELECT
// Filter fast-select wakes to this listener only. If the sock lookup returns nullptr,
// no wakes fire and loop() falls back to the self-disable safety net.
esphome_fast_select_set_ota_listener_sock(esphome_lwip_get_sock(this->server_->get_fd()));
#endif
}
void ESPHomeOTAComponent::dump_config() {
@@ -103,15 +81,13 @@ void ESPHomeOTAComponent::dump_config() {
}
void ESPHomeOTAComponent::loop() {
// Self-disabling idle loop. Runs when a wake path marks us pending-enable (fast-select
// listener filter, raw-TCP accept_fn_, or host select), finds no work, and goes back
// to sleep. cleanup_connection_() deliberately leaves the loop enabled for one more
// iteration so a connection queued mid-session is still caught here.
if (this->client_ == nullptr && !this->server_->ready()) {
this->disable_loop();
return;
// Skip handle_handshake_() call if no client connected and no incoming connections
// This optimization reduces idle loop overhead when OTA is not active
// Note: No need to check server_ for null as the component is marked failed in setup()
// if server_ creation fails
if (this->client_ != nullptr || this->server_->ready()) {
this->handle_handshake_();
}
this->handle_handshake_();
}
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
@@ -590,9 +566,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
// set while we were in LOOP state, would be lost to enable_pending_loops_()).
}
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {

View File

@@ -28,14 +28,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
};
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }
#else
// Stub so lambdas referencing set_auth_password() produce a clear error instead of
// a cryptic "no member" diagnostic. Only fires if the stub is actually instantiated.
template<bool B = false> void set_auth_password(const std::string &) {
static_assert(B, "set_auth_password() requires the OTA auth path to be compiled. "
"Add 'password: \"\"' (empty string) to your 'ota: - platform: esphome' "
"config to enable runtime password rotation.");
}
#endif // USE_OTA_PASSWORD
/// Manually set the port OTA should listen on

View File

@@ -221,7 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI2_HOST};
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif

View File

@@ -325,7 +325,7 @@ def download_gfont(value):
raise cv.Invalid(
f"Could not download font at {url}, please check the fonts exists "
f"at google fonts ({e})"
) from e
)
match = re.search(r"src:\s+url\((.+)\)\s+format\('truetype'\);", req.text)
if match is None:
raise cv.Invalid(

View File

@@ -108,13 +108,8 @@ async def globals_set_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
var = cg.new_Pvariable(action_id, template_arg, paren)
# Use the global's value_type alias as the lambda return type so
# TemplatableFn stores a direct function pointer instead of going through
# the deprecated converting trampoline when the value expression deduces
# to a different type (e.g. int literal assigned to a float global).
value_type = cg.RawExpression(f"{full_id.type}::value_type")
templ = await cg.templatable(
config[CONF_VALUE], args, value_type, to_exp=cg.RawExpression
config[CONF_VALUE], args, None, to_exp=cg.RawExpression, wrap_constant=True
)
cg.add(var.set_value(templ))
return var

View File

@@ -60,73 +60,6 @@ CONFIG_SCHEMA = (
)
def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
"""Check if pin is shared exclusively with deep_sleep (wakeup pin)."""
pin_key = (CORE.target_platform, CORE.target_platform, pin_num)
pin_users = pins.PIN_SCHEMA_REGISTRY.pins_used.get(pin_key, [])
if len(pin_users) != 2:
return False
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
def _final_validate(config):
use_interrupt = config[CONF_USE_INTERRUPT]
if not use_interrupt:
return config
pin_num = config[CONF_PIN][CONF_NUMBER]
# Expander pins (e.g. PCF8574, MCP23017) don't support direct interrupt
# attachment — only internal/native GPIO pins do.
if pins.PIN_SCHEMA_REGISTRY.get_key(config[CONF_PIN]) != CORE.target_platform:
_LOGGER.info(
"GPIO binary_sensor '%s': Pin is not an internal GPIO, "
"falling back to polling mode.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# GPIO16 on ESP8266 doesn't support interrupts through attachInterrupt().
if CORE.is_esp8266 and pin_num == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
config[CONF_USE_INTERRUPT] = False
return config
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes.
# Exception: deep_sleep wakeup pins are compatible with interrupts when
# the pin is only shared between this sensor and deep_sleep (count == 2).
if config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
if not _pin_shared_only_with_deep_sleep(pin_num):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared "
"with other components. The sensor will use polling mode for "
"compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
config[CONF_USE_INTERRUPT] = False
else:
_LOGGER.debug(
"GPIO binary_sensor '%s': Pin %s is shared with deep_sleep, "
"keeping interrupts enabled.",
config.get(CONF_NAME, config[CONF_ID]),
pin_num,
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
@@ -134,7 +67,36 @@ async def to_code(config):
pin = await cg.gpio_pin_expression(config[CONF_PIN])
cg.add(var.set_pin(pin))
if config[CONF_USE_INTERRUPT]:
# Check for ESP8266 GPIO16 interrupt limitation
# GPIO16 on ESP8266 is a special pin that doesn't support interrupts through
# the Arduino attachInterrupt() function. This is the only known GPIO pin
# across all supported platforms that has this limitation, so we handle it
# here instead of in the platform-specific code.
use_interrupt = config[CONF_USE_INTERRUPT]
if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16:
_LOGGER.warning(
"GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. "
"Falling back to polling mode (same as in ESPHome <2025.7). "
"The sensor will work exactly as before, but other pins have better "
"performance with interrupts.",
config.get(CONF_NAME, config[CONF_ID]),
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))
else:
cg.add(var.set_use_interrupt(False))
# Only generate call when disabling interrupts (default is true)
cg.add(var.set_use_interrupt(use_interrupt))

View File

@@ -46,6 +46,11 @@ void GPIOBinarySensorStore::setup(InternalGPIOPin *pin, Component *component) {
}
void GPIOBinarySensor::setup() {
if (this->store_.use_interrupt_ && !this->pin_->is_internal()) {
ESP_LOGD(TAG, "GPIO is not internal, falling back to polling mode");
this->store_.use_interrupt_ = false;
}
if (this->store_.use_interrupt_) {
auto *internal_pin = static_cast<InternalGPIOPin *>(this->pin_);
this->store_.setup(internal_pin, this);

View File

@@ -127,6 +127,6 @@ async def to_code(config):
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_build_flag("-Wno-error=overloaded-virtual")
cg.add_library("tonia/HeatpumpIR", "1.0.41")
cg.add_library("tonia/HeatpumpIR", "1.0.40")
if CORE.is_libretiny or CORE.is_esp32:
CORE.add_platformio_option("lib_ignore", ["IRremoteESP8266"])

View File

@@ -77,8 +77,7 @@ uint32_t arch_get_cpu_freq_hz() { return 1000000000U; }
void setup();
void loop();
int __attribute__((optimize("O2"))) // NOLINT(clang-diagnostic-unknown-attributes)
main() {
int main() {
// Install signal handlers for graceful shutdown (flushes preferences to disk)
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);

View File

@@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() {
}
std::string HttpContainer::get_response_header(const std::string &header_name) {
auto lower = str_lower_case(header_name); // NOLINT
auto lower = str_lower_case(header_name);
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());

View File

@@ -11,7 +11,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -401,7 +400,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
lower.push_back(str_lower_case(h));
}
return this->perform(url, method, body, request_headers, lower);
}
@@ -416,7 +415,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h)); // NOLINT
lower.push_back(str_lower_case(h));
}
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}

View File

@@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->response_headers_.clear();
auto header_count = container->client_.headers();
for (int i = 0; i < header_count; i++) {
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
if (should_collect_header(lower_case_collect_headers, header_name)) {
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());

View File

@@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first); // NOLINT
auto lower_name = str_lower_case(header.first);
if (should_collect_header(lower_case_collect_headers, lower_name)) {
container->response_headers_.push_back({lower_name, header.second});
}

View File

@@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: {
const std::string header_name = str_lower_case(evt->header_key); // NOLINT
const std::string header_name = str_lower_case(evt->header_key);
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());

View File

@@ -36,7 +36,7 @@ I2SAudioMicrophone = i2s_audio_ns.class_(
)
INTERNAL_ADC_VARIANTS = [esp32.VARIANT_ESP32]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3, esp32.VARIANT_ESP32P4]
PDM_VARIANTS = [esp32.VARIANT_ESP32, esp32.VARIANT_ESP32S3]
def _validate_esp32_variant(config):

View File

@@ -33,16 +33,13 @@ AUTO_LOAD = ["audio"]
CODEOWNERS = ["@jesserockz", "@kahrendt"]
DEPENDENCIES = ["i2s_audio"]
I2SAudioSpeakerBase = i2s_audio_ns.class_(
"I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut
I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
)
I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
INTERNAL_DAC_OPTIONS = {
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
@@ -186,11 +183,11 @@ async def to_code(config):
await speaker.register_speaker(var, config)
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = I2SCommFmt.MSB
fmt = "msb"
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = I2SCommFmt.PCM
fmt = "pcm"
cg.add(var.set_i2s_comm_fmt(fmt))
if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT]))

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