Merge remote-tracking branch 'upstream/dev' into loop_runtime_stats

This commit is contained in:
J. Nick Koston 2025-05-27 11:39:45 -05:00
commit c955897d1b
No known key found for this signature in database
260 changed files with 8297 additions and 2191 deletions

View File

@ -1,2 +1,4 @@
[run] [run]
omit = esphome/components/* omit =
esphome/components/*
tests/integration/*

37
.devcontainer/Dockerfile Normal file
View File

@ -0,0 +1,37 @@
ARG BUILD_BASE_VERSION=2025.04.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
RUN git config --system --add safe.directory "*"
RUN apt update \
&& apt install -y \
protobuf-compiler
RUN pip install uv
RUN useradd esphome -m
USER esphome
ENV VIRTUAL_ENV=/home/esphome/.local/esphome-venv
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Override this set to true in the docker-base image
ENV UV_SYSTEM_PYTHON=false
WORKDIR /tmp
COPY requirements.txt ./
RUN uv pip install -r requirements.txt
COPY requirements_dev.txt requirements_test.txt ./
RUN uv pip install -r requirements_dev.txt -r requirements_test.txt
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000
COPY script/platformio_install_deps.py platformio.ini ./
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
WORKDIR /workspaces

View File

@ -1,18 +1,17 @@
{ {
"name": "ESPHome Dev", "name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev", "context": "..",
"dockerFile": "Dockerfile",
"postCreateCommand": [ "postCreateCommand": [
"script/devcontainer-post-create" "script/devcontainer-post-create"
], ],
"containerEnv": { "features": {
"DEVCONTAINER": "1", "ghcr.io/devcontainers/features/github-cli:1": {}
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
}, },
"runArgs": [ "runArgs": [
"--privileged", "--privileged",
"-e", "-e",
"ESPHOME_DASHBOARD_USE_PING=1" "GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass though local USB serial to the conatiner // uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0" // , "--device=/dev/ttyACM0"
], ],

View File

@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest - name: Build and push to ghcr by digest
id: build-ghcr id: build-ghcr
uses: docker/build-push-action@v6.16.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false
@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest - name: Build and push to dockerhub by digest
id: build-dockerhub id: build-dockerhub
uses: docker/build-push-action@v6.16.0 uses: docker/build-push-action@v6.17.0
env: env:
DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false DOCKER_BUILD_RECORD_UPLOAD: false

View File

@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:

View File

@ -43,11 +43,11 @@ jobs:
- "docker" - "docker"
# - "lint" # - "lint"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.10"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0

View File

@ -20,8 +20,8 @@ permissions:
contents: read contents: read
env: env:
DEFAULT_PYTHON: "3.9" DEFAULT_PYTHON: "3.10"
PYUPGRADE_TARGET: "--py39-plus" PYUPGRADE_TARGET: "--py310-plus"
concurrency: concurrency:
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
@ -36,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }} cache-key: ${{ steps.cache-key.outputs.key }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Generate cache-key - name: Generate cache-key
id: cache-key id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
@ -68,7 +68,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -89,7 +89,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -110,7 +110,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -131,7 +131,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -152,7 +152,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -173,10 +173,10 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
python-version: python-version:
- "3.9"
- "3.10" - "3.10"
- "3.11" - "3.11"
- "3.12" - "3.12"
- "3.13"
os: os:
- ubuntu-latest - ubuntu-latest
- macOS-latest - macOS-latest
@ -185,24 +185,24 @@ jobs:
# Minimize CI resource usage # Minimize CI resource usage
# by only running the Python version # by only running the Python version
# version used for docker images on Windows and macOS # version used for docker images on Windows and macOS
- python-version: "3.13"
os: windows-latest
- python-version: "3.12" - python-version: "3.12"
os: windows-latest os: windows-latest
- python-version: "3.10" - python-version: "3.10"
os: windows-latest os: windows-latest
- python-version: "3.9" - python-version: "3.13"
os: windows-latest os: macOS-latest
- python-version: "3.12" - python-version: "3.12"
os: macOS-latest os: macOS-latest
- python-version: "3.10" - python-version: "3.10"
os: macOS-latest os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
needs: needs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -214,14 +214,14 @@ jobs:
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
run: | run: |
./venv/Scripts/activate ./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native tests pytest -vv --cov-report=xml --tb=native -n auto tests
- name: Run pytest - name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest' if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: | run: |
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests pytest -vv --cov-report=xml --tb=native -n auto tests
- name: Upload coverage to Codecov - name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.2 uses: codecov/codecov-action@v5.4.3
with: with:
token: ${{ secrets.CODECOV_TOKEN }} token: ${{ secrets.CODECOV_TOKEN }}
@ -232,7 +232,7 @@ jobs:
- common - common
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -300,7 +300,7 @@ jobs:
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -356,7 +356,7 @@ jobs:
count: ${{ steps.list-components.outputs.count }} count: ${{ steps.list-components.outputs.count }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
with: with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works. # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500 fetch-depth: 500
@ -406,7 +406,7 @@ jobs:
sudo apt-get install libsdl2-dev sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:
@ -432,7 +432,7 @@ jobs:
matrix: ${{ steps.split.outputs.components }} matrix: ${{ steps.split.outputs.components }}
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Split components into 20 groups - name: Split components into 20 groups
id: split id: split
run: | run: |
@ -462,7 +462,7 @@ jobs:
sudo apt-get install libsdl2-dev sudo apt-get install libsdl2-dev
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Restore Python - name: Restore Python
uses: ./.github/actions/restore-python uses: ./.github/actions/restore-python
with: with:

View File

@ -18,8 +18,9 @@ jobs:
outputs: outputs:
tag: ${{ steps.tag.outputs.tag }} tag: ${{ steps.tag.outputs.tag }}
branch_build: ${{ steps.tag.outputs.branch_build }} branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.2.2
- name: Get tag - name: Get tag
id: tag id: tag
# yamllint disable rule:line-length # yamllint disable rule:line-length
@ -27,6 +28,11 @@ jobs:
if [[ "${{ github.event_name }}" = "release" ]]; then if [[ "${{ github.event_name }}" = "release" ]]; then
TAG="${{ github.event.release.tag_name}}" TAG="${{ github.event.release.tag_name}}"
BRANCH_BUILD="false" BRANCH_BUILD="false"
if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
ENVIRONMENT="beta"
else
ENVIRONMENT="production"
fi
else else
TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
today="$(date --utc '+%Y%m%d')" today="$(date --utc '+%Y%m%d')"
@ -35,12 +41,15 @@ jobs:
if [[ "$BRANCH" != "dev" ]]; then if [[ "$BRANCH" != "dev" ]]; then
TAG="${TAG}-${BRANCH}" TAG="${TAG}-${BRANCH}"
BRANCH_BUILD="true" BRANCH_BUILD="true"
ENVIRONMENT=""
else else
BRANCH_BUILD="false" BRANCH_BUILD="false"
ENVIRONMENT="dev"
fi fi
fi fi
echo "tag=${TAG}" >> $GITHUB_OUTPUT echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
# yamllint enable rule:line-length # yamllint enable rule:line-length
deploy-pypi: deploy-pypi:
@ -51,7 +60,7 @@ jobs:
contents: read contents: read
id-token: write id-token: write
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
@ -83,11 +92,11 @@ jobs:
os: "ubuntu-24.04-arm" os: "ubuntu-24.04-arm"
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.2.2
- name: Set up Python - name: Set up Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.9" python-version: "3.10"
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0 uses: docker/setup-buildx-action@v3.10.0
@ -159,7 +168,7 @@ jobs:
- ghcr - ghcr
- dockerhub - dockerhub
steps: steps:
- uses: actions/checkout@v4.1.7 - uses: actions/checkout@v4.2.2
- name: Download digests - name: Download digests
uses: actions/download-artifact@v4.3.0 uses: actions/download-artifact@v4.3.0
@ -233,9 +242,8 @@ jobs:
deploy-esphome-schema: deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false' if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: needs: [init]
- init environment: ${{ needs.init.outputs.deploy_env }}
- deploy-manifest
steps: steps:
- name: Trigger Workflow - name: Trigger Workflow
uses: actions/github-script@v7.0.1 uses: actions/github-script@v7.0.1

View File

@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome' if: github.repository == 'esphome/esphome'
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Checkout Home Assistant - name: Checkout Home Assistant
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
with: with:
repository: home-assistant/core repository: home-assistant/core
path: lib/home-assistant path: lib/home-assistant
@ -24,7 +24,7 @@ jobs:
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: 3.12 python-version: 3.13
- name: Install Home Assistant - name: Install Home Assistant
run: | run: |

View File

@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
uses: actions/checkout@v4.1.7 uses: actions/checkout@v4.2.2
- name: Run yamllint - name: Run yamllint
uses: frenck/action-yamllint@v1.5.0 uses: frenck/action-yamllint@v1.5.0
with: with:

3
.gitignore vendored
View File

@ -143,5 +143,4 @@ sdkconfig.*
/components /components
/managed_components /managed_components
api-docs/
**/.claude/settings.local.json

View File

@ -4,7 +4,7 @@
repos: repos:
- repo: https://github.com/astral-sh/ruff-pre-commit - repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: v0.11.9 rev: v0.11.10
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@ -28,10 +28,10 @@ repos:
- --branch=release - --branch=release
- --branch=beta - --branch=beta
- repo: https://github.com/asottile/pyupgrade - repo: https://github.com/asottile/pyupgrade
rev: v3.15.2 rev: v3.20.0
hooks: hooks:
- id: pyupgrade - id: pyupgrade
args: [--py39-plus] args: [--py310-plus]
- repo: https://github.com/adrienverge/yamllint.git - repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1 rev: v1.37.1
hooks: hooks:

View File

@ -96,6 +96,7 @@ esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/chsc6x/* @kkosik20 esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet esphome/components/climate_ir/* @glmnet
esphome/components/cm1106/* @andrewjswan
esphome/components/color_temperature/* @jesserockz esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core esphome/components/const/* @esphome/core
@ -169,7 +170,7 @@ esphome/components/gp2y1010au0f/* @zry98
esphome/components/gp8403/* @jesserockz esphome/components/gp8403/* @jesserockz
esphome/components/gpio/* @esphome/core esphome/components/gpio/* @esphome/core
esphome/components/gpio/one_wire/* @ssieb esphome/components/gpio/one_wire/* @ssieb
esphome/components/gps/* @coogle esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers esphome/components/gree/* @orestismers
@ -478,6 +479,8 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core esphome/components/valve/* @esphome/core
esphome/components/vbus/* @ssieb esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81 esphome/components/veml3235/* @kbx81

2877
Doxyfile Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,9 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*" RUN git config --system --add safe.directory "*"
RUN pip install uv==0.6.14 ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14
COPY requirements.txt / COPY requirements.txt /

View File

@ -43,7 +43,7 @@ from esphome.const import (
) )
from esphome.core import CORE, EsphomeError, coroutine from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, indent, is_ip_address from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import Fore, color, setup_log from esphome.log import AnsiFore, color, setup_log
from esphome.util import ( from esphome.util import (
get_serial_ports, get_serial_ports,
list_yaml_files, list_yaml_files,
@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
raise ValueError raise ValueError
break break
except ValueError: except ValueError:
safe_print(color(Fore.RED, f"Invalid option: '{opt}'")) safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
return options[opt - 1][1] return options[opt - 1][1]
@ -596,30 +596,30 @@ def command_update_all(args):
click.echo(f"{half_line}{middle_text}{half_line}") click.echo(f"{half_line}{middle_text}{half_line}")
for f in files: for f in files:
print(f"Updating {color(Fore.CYAN, f)}") print(f"Updating {color(AnsiFore.CYAN, f)}")
print("-" * twidth) print("-" * twidth)
print() print()
rc = run_external_process( rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
) )
if rc == 0: if rc == 0:
print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
success[f] = True success[f] = True
else: else:
print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
success[f] = False success[f] = False
print() print()
print() print()
print() print()
print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0 failed = 0
for f in files: for f in files:
if success[f]: if success[f]:
print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}") print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else: else:
print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}") print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1 failed += 1
return failed return failed
@ -645,7 +645,7 @@ def command_rename(args, config):
if c not in ALLOWED_NAME_CHARS: if c not in ALLOWED_NAME_CHARS:
print( print(
color( color(
Fore.BOLD_RED, AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: " f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
) )
@ -658,7 +658,9 @@ def command_rename(args, config):
yaml = yaml_util.load_yaml(CORE.config_path) yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print( print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
) )
return 1 return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME] old_name = yaml[CONF_ESPHOME][CONF_NAME]
@ -681,7 +683,7 @@ def command_rename(args, config):
) )
> 1 > 1
): ):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1 return 1
new_raw = re.sub( new_raw = re.sub(
@ -693,7 +695,7 @@ def command_rename(args, config):
new_path = os.path.join(CORE.config_dir, args.name + ".yaml") new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print( print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
) )
print() print()
@ -702,7 +704,7 @@ def command_rename(args, config):
rc = run_external_process("esphome", "config", new_path) rc = run_external_process("esphome", "config", new_path)
if rc != 0: if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path) os.remove(new_path)
return 1 return 1
@ -728,7 +730,7 @@ def command_rename(args, config):
if CORE.config_path != new_path: if CORE.config_path != new_path:
os.remove(CORE.config_path) os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS")) print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print() print()
return 0 return 0

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT from esphome.const import CONF_UNIT_OF_MEASUREMENT
UNITS = { UNITS = {
"f": "f", "f": "f",
@ -17,9 +17,9 @@ Anova = anova_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(Anova)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Anova),
cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
} }
) )
@ -29,8 +29,7 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))

View File

@ -432,7 +432,8 @@ message FanCommandRequest {
enum ColorMode { enum ColorMode {
COLOR_MODE_UNKNOWN = 0; COLOR_MODE_UNKNOWN = 0;
COLOR_MODE_ON_OFF = 1; COLOR_MODE_ON_OFF = 1;
COLOR_MODE_BRIGHTNESS = 2; COLOR_MODE_LEGACY_BRIGHTNESS = 2;
COLOR_MODE_BRIGHTNESS = 3;
COLOR_MODE_WHITE = 7; COLOR_MODE_WHITE = 7;
COLOR_MODE_COLOR_TEMPERATURE = 11; COLOR_MODE_COLOR_TEMPERATURE = 11;
COLOR_MODE_COLD_WARM_WHITE = 19; COLOR_MODE_COLD_WARM_WHITE = 19;

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,17 @@
#include "api_server.h" #include "api_server.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include <vector> #include <vector>
namespace esphome { namespace esphome {
namespace api { namespace api {
using send_message_t = bool(APIConnection *, void *); // Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
using send_message_t = bool (APIConnection::*)(void *);
/* /*
This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that This class holds a pointer to the source component that wants to publish a message, and a pointer to a function that
@ -30,10 +34,10 @@ class DeferredMessageQueue {
protected: protected:
void *source_; void *source_;
send_message_t *send_message_; send_message_t send_message_;
public: public:
DeferredMessage(void *source, send_message_t *send_message) : source_(source), send_message_(send_message) {} DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
bool operator==(const DeferredMessage &test) const { bool operator==(const DeferredMessage &test) const {
return (source_ == test.source_ && send_message_ == test.send_message_); return (source_ == test.source_ && send_message_ == test.send_message_);
} }
@ -46,12 +50,13 @@ class DeferredMessageQueue {
APIConnection *api_connection_; APIConnection *api_connection_;
// helper for allowing only unique entries in the queue // helper for allowing only unique entries in the queue
void dmq_push_back_with_dedup_(void *source, send_message_t *send_message); void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
public: public:
DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {} DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
void process_queue(); void process_queue();
void defer(void *source, send_message_t *send_message); void defer(void *source, send_message_t send_message);
bool empty() const { return deferred_queue_.empty(); }
}; };
class APIConnection : public APIServerConnection { class APIConnection : public APIServerConnection {
@ -69,137 +74,213 @@ class APIConnection : public APIServerConnection {
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state); bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor); void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, void *v_binary_sensor);
static bool try_send_binary_sensor_state(APIConnection *api, binary_sensor::BinarySensor *binary_sensor, bool state); protected:
static bool try_send_binary_sensor_info(APIConnection *api, void *v_binary_sensor); bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor);
bool try_send_binary_sensor_state_(binary_sensor::BinarySensor *binary_sensor, bool state);
bool try_send_binary_sensor_info_(binary_sensor::BinarySensor *binary_sensor);
public:
#endif #endif
#ifdef USE_COVER #ifdef USE_COVER
bool send_cover_state(cover::Cover *cover); bool send_cover_state(cover::Cover *cover);
void send_cover_info(cover::Cover *cover); void send_cover_info(cover::Cover *cover);
static bool try_send_cover_state(APIConnection *api, void *v_cover);
static bool try_send_cover_info(APIConnection *api, void *v_cover);
void cover_command(const CoverCommandRequest &msg) override; void cover_command(const CoverCommandRequest &msg) override;
protected:
bool try_send_cover_state_(cover::Cover *cover);
bool try_send_cover_info_(cover::Cover *cover);
public:
#endif #endif
#ifdef USE_FAN #ifdef USE_FAN
bool send_fan_state(fan::Fan *fan); bool send_fan_state(fan::Fan *fan);
void send_fan_info(fan::Fan *fan); void send_fan_info(fan::Fan *fan);
static bool try_send_fan_state(APIConnection *api, void *v_fan);
static bool try_send_fan_info(APIConnection *api, void *v_fan);
void fan_command(const FanCommandRequest &msg) override; void fan_command(const FanCommandRequest &msg) override;
protected:
bool try_send_fan_state_(fan::Fan *fan);
bool try_send_fan_info_(fan::Fan *fan);
public:
#endif #endif
#ifdef USE_LIGHT #ifdef USE_LIGHT
bool send_light_state(light::LightState *light); bool send_light_state(light::LightState *light);
void send_light_info(light::LightState *light); void send_light_info(light::LightState *light);
static bool try_send_light_state(APIConnection *api, void *v_light);
static bool try_send_light_info(APIConnection *api, void *v_light);
void light_command(const LightCommandRequest &msg) override; void light_command(const LightCommandRequest &msg) override;
protected:
bool try_send_light_state_(light::LightState *light);
bool try_send_light_info_(light::LightState *light);
public:
#endif #endif
#ifdef USE_SENSOR #ifdef USE_SENSOR
bool send_sensor_state(sensor::Sensor *sensor, float state); bool send_sensor_state(sensor::Sensor *sensor, float state);
void send_sensor_info(sensor::Sensor *sensor); void send_sensor_info(sensor::Sensor *sensor);
static bool try_send_sensor_state(APIConnection *api, void *v_sensor);
static bool try_send_sensor_state(APIConnection *api, sensor::Sensor *sensor, float state); protected:
static bool try_send_sensor_info(APIConnection *api, void *v_sensor); bool try_send_sensor_state_(sensor::Sensor *sensor);
bool try_send_sensor_state_(sensor::Sensor *sensor, float state);
bool try_send_sensor_info_(sensor::Sensor *sensor);
public:
#endif #endif
#ifdef USE_SWITCH #ifdef USE_SWITCH
bool send_switch_state(switch_::Switch *a_switch, bool state); bool send_switch_state(switch_::Switch *a_switch, bool state);
void send_switch_info(switch_::Switch *a_switch); void send_switch_info(switch_::Switch *a_switch);
static bool try_send_switch_state(APIConnection *api, void *v_a_switch);
static bool try_send_switch_state(APIConnection *api, switch_::Switch *a_switch, bool state);
static bool try_send_switch_info(APIConnection *api, void *v_a_switch);
void switch_command(const SwitchCommandRequest &msg) override; void switch_command(const SwitchCommandRequest &msg) override;
protected:
bool try_send_switch_state_(switch_::Switch *a_switch);
bool try_send_switch_state_(switch_::Switch *a_switch, bool state);
bool try_send_switch_info_(switch_::Switch *a_switch);
public:
#endif #endif
#ifdef USE_TEXT_SENSOR #ifdef USE_TEXT_SENSOR
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state); bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
void send_text_sensor_info(text_sensor::TextSensor *text_sensor); void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, void *v_text_sensor);
static bool try_send_text_sensor_state(APIConnection *api, text_sensor::TextSensor *text_sensor, std::string state); protected:
static bool try_send_text_sensor_info(APIConnection *api, void *v_text_sensor); bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor);
bool try_send_text_sensor_state_(text_sensor::TextSensor *text_sensor, std::string state);
bool try_send_text_sensor_info_(text_sensor::TextSensor *text_sensor);
public:
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image); void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
void send_camera_info(esp32_camera::ESP32Camera *camera); void send_camera_info(esp32_camera::ESP32Camera *camera);
static bool try_send_camera_info(APIConnection *api, void *v_camera);
void camera_image(const CameraImageRequest &msg) override; void camera_image(const CameraImageRequest &msg) override;
protected:
bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
public:
#endif #endif
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate); bool send_climate_state(climate::Climate *climate);
void send_climate_info(climate::Climate *climate); void send_climate_info(climate::Climate *climate);
static bool try_send_climate_state(APIConnection *api, void *v_climate);
static bool try_send_climate_info(APIConnection *api, void *v_climate);
void climate_command(const ClimateCommandRequest &msg) override; void climate_command(const ClimateCommandRequest &msg) override;
protected:
bool try_send_climate_state_(climate::Climate *climate);
bool try_send_climate_info_(climate::Climate *climate);
public:
#endif #endif
#ifdef USE_NUMBER #ifdef USE_NUMBER
bool send_number_state(number::Number *number, float state); bool send_number_state(number::Number *number, float state);
void send_number_info(number::Number *number); void send_number_info(number::Number *number);
static bool try_send_number_state(APIConnection *api, void *v_number);
static bool try_send_number_state(APIConnection *api, number::Number *number, float state);
static bool try_send_number_info(APIConnection *api, void *v_number);
void number_command(const NumberCommandRequest &msg) override; void number_command(const NumberCommandRequest &msg) override;
protected:
bool try_send_number_state_(number::Number *number);
bool try_send_number_state_(number::Number *number, float state);
bool try_send_number_info_(number::Number *number);
public:
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
bool send_date_state(datetime::DateEntity *date); bool send_date_state(datetime::DateEntity *date);
void send_date_info(datetime::DateEntity *date); void send_date_info(datetime::DateEntity *date);
static bool try_send_date_state(APIConnection *api, void *v_date);
static bool try_send_date_info(APIConnection *api, void *v_date);
void date_command(const DateCommandRequest &msg) override; void date_command(const DateCommandRequest &msg) override;
protected:
bool try_send_date_state_(datetime::DateEntity *date);
bool try_send_date_info_(datetime::DateEntity *date);
public:
#endif #endif
#ifdef USE_DATETIME_TIME #ifdef USE_DATETIME_TIME
bool send_time_state(datetime::TimeEntity *time); bool send_time_state(datetime::TimeEntity *time);
void send_time_info(datetime::TimeEntity *time); void send_time_info(datetime::TimeEntity *time);
static bool try_send_time_state(APIConnection *api, void *v_time);
static bool try_send_time_info(APIConnection *api, void *v_time);
void time_command(const TimeCommandRequest &msg) override; void time_command(const TimeCommandRequest &msg) override;
protected:
bool try_send_time_state_(datetime::TimeEntity *time);
bool try_send_time_info_(datetime::TimeEntity *time);
public:
#endif #endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
bool send_datetime_state(datetime::DateTimeEntity *datetime); bool send_datetime_state(datetime::DateTimeEntity *datetime);
void send_datetime_info(datetime::DateTimeEntity *datetime); void send_datetime_info(datetime::DateTimeEntity *datetime);
static bool try_send_datetime_state(APIConnection *api, void *v_datetime);
static bool try_send_datetime_info(APIConnection *api, void *v_datetime);
void datetime_command(const DateTimeCommandRequest &msg) override; void datetime_command(const DateTimeCommandRequest &msg) override;
protected:
bool try_send_datetime_state_(datetime::DateTimeEntity *datetime);
bool try_send_datetime_info_(datetime::DateTimeEntity *datetime);
public:
#endif #endif
#ifdef USE_TEXT #ifdef USE_TEXT
bool send_text_state(text::Text *text, std::string state); bool send_text_state(text::Text *text, std::string state);
void send_text_info(text::Text *text); void send_text_info(text::Text *text);
static bool try_send_text_state(APIConnection *api, void *v_text);
static bool try_send_text_state(APIConnection *api, text::Text *text, std::string state);
static bool try_send_text_info(APIConnection *api, void *v_text);
void text_command(const TextCommandRequest &msg) override; void text_command(const TextCommandRequest &msg) override;
protected:
bool try_send_text_state_(text::Text *text);
bool try_send_text_state_(text::Text *text, std::string state);
bool try_send_text_info_(text::Text *text);
public:
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
bool send_select_state(select::Select *select, std::string state); bool send_select_state(select::Select *select, std::string state);
void send_select_info(select::Select *select); void send_select_info(select::Select *select);
static bool try_send_select_state(APIConnection *api, void *v_select);
static bool try_send_select_state(APIConnection *api, select::Select *select, std::string state);
static bool try_send_select_info(APIConnection *api, void *v_select);
void select_command(const SelectCommandRequest &msg) override; void select_command(const SelectCommandRequest &msg) override;
protected:
bool try_send_select_state_(select::Select *select);
bool try_send_select_state_(select::Select *select, std::string state);
bool try_send_select_info_(select::Select *select);
public:
#endif #endif
#ifdef USE_BUTTON #ifdef USE_BUTTON
void send_button_info(button::Button *button); void send_button_info(button::Button *button);
static bool try_send_button_info(APIConnection *api, void *v_button);
void button_command(const ButtonCommandRequest &msg) override; void button_command(const ButtonCommandRequest &msg) override;
protected:
bool try_send_button_info_(button::Button *button);
public:
#endif #endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_lock_state(lock::Lock *a_lock, lock::LockState state); bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
void send_lock_info(lock::Lock *a_lock); void send_lock_info(lock::Lock *a_lock);
static bool try_send_lock_state(APIConnection *api, void *v_a_lock);
static bool try_send_lock_state(APIConnection *api, lock::Lock *a_lock, lock::LockState state);
static bool try_send_lock_info(APIConnection *api, void *v_a_lock);
void lock_command(const LockCommandRequest &msg) override; void lock_command(const LockCommandRequest &msg) override;
protected:
bool try_send_lock_state_(lock::Lock *a_lock);
bool try_send_lock_state_(lock::Lock *a_lock, lock::LockState state);
bool try_send_lock_info_(lock::Lock *a_lock);
public:
#endif #endif
#ifdef USE_VALVE #ifdef USE_VALVE
bool send_valve_state(valve::Valve *valve); bool send_valve_state(valve::Valve *valve);
void send_valve_info(valve::Valve *valve); void send_valve_info(valve::Valve *valve);
static bool try_send_valve_state(APIConnection *api, void *v_valve);
static bool try_send_valve_info(APIConnection *api, void *v_valve);
void valve_command(const ValveCommandRequest &msg) override; void valve_command(const ValveCommandRequest &msg) override;
protected:
bool try_send_valve_state_(valve::Valve *valve);
bool try_send_valve_info_(valve::Valve *valve);
public:
#endif #endif
#ifdef USE_MEDIA_PLAYER #ifdef USE_MEDIA_PLAYER
bool send_media_player_state(media_player::MediaPlayer *media_player); bool send_media_player_state(media_player::MediaPlayer *media_player);
void send_media_player_info(media_player::MediaPlayer *media_player); void send_media_player_info(media_player::MediaPlayer *media_player);
static bool try_send_media_player_state(APIConnection *api, void *v_media_player);
static bool try_send_media_player_info(APIConnection *api, void *v_media_player);
void media_player_command(const MediaPlayerCommandRequest &msg) override; void media_player_command(const MediaPlayerCommandRequest &msg) override;
protected:
bool try_send_media_player_state_(media_player::MediaPlayer *media_player);
bool try_send_media_player_info_(media_player::MediaPlayer *media_player);
public:
#endif #endif
bool try_send_log_message(int level, const char *tag, const char *line); bool try_send_log_message(int level, const char *tag, const char *line);
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
@ -246,25 +327,37 @@ class APIConnection : public APIServerConnection {
#ifdef USE_ALARM_CONTROL_PANEL #ifdef USE_ALARM_CONTROL_PANEL
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); void send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
static bool try_send_alarm_control_panel_state(APIConnection *api, void *v_a_alarm_control_panel);
static bool try_send_alarm_control_panel_info(APIConnection *api, void *v_a_alarm_control_panel);
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
protected:
bool try_send_alarm_control_panel_state_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
bool try_send_alarm_control_panel_info_(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
public:
#endif #endif
#ifdef USE_EVENT #ifdef USE_EVENT
void send_event(event::Event *event, std::string event_type); void send_event(event::Event *event, std::string event_type);
void send_event_info(event::Event *event); void send_event_info(event::Event *event);
static bool try_send_event(APIConnection *api, void *v_event);
static bool try_send_event(APIConnection *api, event::Event *event, std::string event_type); protected:
static bool try_send_event_info(APIConnection *api, void *v_event); bool try_send_event_(event::Event *event);
bool try_send_event_(event::Event *event, std::string event_type);
bool try_send_event_info_(event::Event *event);
public:
#endif #endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
bool send_update_state(update::UpdateEntity *update); bool send_update_state(update::UpdateEntity *update);
void send_update_info(update::UpdateEntity *update); void send_update_info(update::UpdateEntity *update);
static bool try_send_update_state(APIConnection *api, void *v_update);
static bool try_send_update_info(APIConnection *api, void *v_update);
void update_command(const UpdateCommandRequest &msg) override; void update_command(const UpdateCommandRequest &msg) override;
protected:
bool try_send_update_state_(update::UpdateEntity *update);
bool try_send_update_info_(update::UpdateEntity *update);
public:
#endif #endif
void on_disconnect_response(const DisconnectResponse &value) override; void on_disconnect_response(const DisconnectResponse &value) override;
@ -315,9 +408,17 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override { ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen // FIXME: ensure no recursive writes can happen
this->proto_write_buffer_.clear(); this->proto_write_buffer_.clear();
this->proto_write_buffer_.reserve(reserve_size); // Get header padding size - used for both reserve and insert
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve space for header padding + message + footer
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
this->proto_write_buffer_.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
this->proto_write_buffer_.insert(this->proto_write_buffer_.begin(), header_padding, 0);
return {&this->proto_write_buffer_}; return {&this->proto_write_buffer_};
} }
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
std::string get_client_combined_info() const { return this->client_combined_info_; } std::string get_client_combined_info() const { return this->client_combined_info_; }
@ -325,6 +426,99 @@ class APIConnection : public APIServerConnection {
protected: protected:
friend APIServer; friend APIServer;
/**
* Generic send entity state method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This is the base version for entities that use their current state.
*
* @param entity The entity to send state for
* @param try_send_func The function that tries to send the state
* @return True on success or message deferred, false if subscription check failed
*/
bool send_state_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return true;
}
this->deferred_message_queue_.defer(entity, try_send_func);
return true;
}
/**
* Send entity state method that handles explicit state values.
* Only attempts to build and send the message if the transmit buffer is available.
*
* This method accepts a state parameter to be used instead of the entity's current state.
* It attempts to send the state with the provided value first, and if that fails due to buffer constraints,
* it defers the entity for later processing using the entity-only function.
*
* @tparam EntityT The entity type
* @tparam StateT Type of the state parameter
* @tparam Args Additional argument types (if any)
* @param entity The entity to send state for
* @param try_send_entity_func The function that tries to send the state with entity pointer only
* @param try_send_state_func The function that tries to send the state with entity and state parameters
* @param state The state value to send
* @param args Additional arguments to pass to the try_send_state_func
* @return True on success or message deferred, false if subscription check failed
*/
template<typename EntityT, typename StateT, typename... Args>
bool send_state_with_value_(EntityT *entity, bool (APIConnection::*try_send_entity_func)(EntityT *),
bool (APIConnection::*try_send_state_func)(EntityT *, StateT, Args...), StateT state,
Args... args) {
if (!this->state_subscription_)
return false;
if (this->try_to_clear_buffer(true) && (this->*try_send_state_func)(entity, state, args...)) {
return true;
}
this->deferred_message_queue_.defer(entity, reinterpret_cast<send_message_t>(try_send_entity_func));
return true;
}
/**
* Generic send entity info method to reduce code duplication.
* Only attempts to build and send the message if the transmit buffer is available.
*
* @param entity The entity to send info for
* @param try_send_func The function that tries to send the info
*/
void send_info_(esphome::EntityBase *entity, send_message_t try_send_func) {
if (this->try_to_clear_buffer(true) && (this->*try_send_func)(entity)) {
return;
}
this->deferred_message_queue_.defer(entity, try_send_func);
}
/**
* Generic function for generating entity info response messages.
* This is used to reduce duplication in the try_send_*_info functions.
*
* @param entity The entity to generate info for
* @param response The response object
* @param send_response_func Function pointer to send the response
* @return True if the message was sent successfully
*/
template<typename ResponseT>
bool try_send_entity_info_(esphome::EntityBase *entity, ResponseT &response,
bool (APIServerConnectionBase::*send_response_func)(const ResponseT &)) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id();
if (entity->has_own_name())
response.name = entity->get_name();
// Set common EntityBase properties
response.icon = entity->get_icon();
response.disabled_by_default = entity->is_disabled_by_default();
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
// Send the response using the provided send method
return (this->*send_response_func)(response);
}
bool send_(const void *buf, size_t len, bool force); bool send_(const void *buf, size_t len, bool force);
enum class ConnectionState { enum class ConnectionState {

View File

@ -7,20 +7,13 @@
#include "proto.h" #include "proto.h"
#include "api_pb2_size.h" #include "api_pb2_size.h"
#include <cstring> #include <cstring>
#include <cinttypes>
namespace esphome { namespace esphome {
namespace api { namespace api {
static const char *const TAG = "api.socket"; static const char *const TAG = "api.socket";
/// Is the given return value (from write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) { const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it // not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) { if (err == APIError::OK) {
@ -73,92 +66,154 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN"; return "UNKNOWN";
} }
// Common implementation for writing raw data to socket // Helper method to buffer data from IOVs
template<typename StateEnum> void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, SendBuffer buffer;
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state, buffer.data.reserve(total_write_len);
StateEnum failed_state) { for (int i = 0; i < iovcnt; i++) {
// This method writes data to socket or buffers it const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
}
this->tx_buf_.push_back(std::move(buffer));
}
// This method writes data to socket or buffers it
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
// Returns APIError::OK if successful (or would block, but data has been buffered) // Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0) if (iovcnt == 0)
return APIError::OK; // Nothing to do, success return APIError::OK; // Nothing to do, success
size_t total_write_len = 0; uint16_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS #ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s", ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif #endif
total_write_len += iov[i].iov_len; total_write_len += static_cast<uint16_t>(iov[i].iov_len);
} }
if (!tx_buf.empty()) { // Try to send any existing buffered data first if there is any
// try to empty tx_buf first if (!this->tx_buf_.empty()) {
while (!tx_buf.empty()) { APIError send_result = try_send_tx_buf_();
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size()); // If real error occurred (not just WOULD_BLOCK), return it
if (is_would_block(sent)) { if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
break; return send_result;
} else if (sent == -1) { }
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state; // If there is still data in the buffer, we can't send, buffer
return APIError::SOCKET_WRITE_FAILED; // Socket write failed // the new data and return
} if (!this->tx_buf_.empty()) {
// TODO: inefficient if multiple packets in txbuf this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
// replace with deque of buffers return APIError::OK; // Success, data buffered
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
} }
} }
if (!tx_buf.empty()) { // Try to send directly if no buffered data
// tx buf not empty, can't write now because then stream would be inconsistent ssize_t sent = this->socket_->writev(iov, iovcnt);
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
}
ssize_t sent = socket->writev(iov, iovcnt); if (sent == -1) {
if (is_would_block(sent)) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
// operation would block, add buffer to tx_buf // Socket would block, buffer the data
// Reserve space upfront to avoid multiple reallocations this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
tx_buf.reserve(tx_buf.size() + total_write_len); return APIError::OK; // Success, data buffered
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
} }
return APIError::OK; // Success, data buffered // Socket error
} else if (sent == -1) { ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
// an error occurred this->state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed return APIError::SOCKET_WRITE_FAILED; // Socket write failed
} else if ((size_t) sent != total_write_len) { } else if (static_cast<uint16_t>(sent) < total_write_len) {
// partially sent, add end to tx_buf // Partially sent, buffer the remaining data
size_t remaining = total_write_len - sent; SendBuffer buffer;
// Reserve space upfront to avoid multiple reallocations uint16_t to_consume = static_cast<uint16_t>(sent);
tx_buf.reserve(tx_buf.size() + remaining); uint16_t remaining = total_write_len - static_cast<uint16_t>(sent);
buffer.data.reserve(remaining);
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) { for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) { if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len; // This segment was fully sent
to_consume -= static_cast<uint16_t>(iov[i].iov_len);
} else { } else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume, // This segment was partially sent or not sent at all
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_consume;
buffer.data.insert(buffer.data.end(), data, data + len);
to_consume = 0; to_consume = 0;
} }
} }
return APIError::OK; // Success, data buffered
this->tx_buf_.push_back(std::move(buffer));
} }
return APIError::OK; // Success, all data sent
return APIError::OK; // Success, all data sent or buffered
} }
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) // Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false;
while (!tx_buf_empty) {
// Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front();
// Try to send the remaining data in this buffer
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
if (sent == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
// Real socket error (not just would block)
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// Socket would block, we'll try again later
return APIError::WOULD_BLOCK;
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front();
// Update empty status for the loop condition
tx_buf_empty = this->tx_buf_.empty();
// Continue loop to try sending the next buffer
}
}
return APIError::OK; // All buffers sent successfully
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_);
return APIError::BAD_STATE;
}
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NODELAY_FAILED;
}
return APIError::OK;
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
@ -206,23 +261,9 @@ std::string noise_err_to_str(int err) {
/// Initialize the frame helper, returns OK if successful. /// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() { APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) { APIError err = init_common_();
HELPER_LOG("Bad state for init %d", (int) state_); if (err != APIError::OK) {
return APIError::BAD_STATE; return err;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
} }
// init prologue // init prologue
@ -234,17 +275,16 @@ APIError APINoiseFrameHelper::init() {
/// Run through handshake messages (if in that phase) /// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() { APIError APINoiseFrameHelper::loop() {
APIError err = state_action_(); APIError err = state_action_();
if (err == APIError::WOULD_BLOCK) if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return APIError::OK;
if (err != APIError::OK)
return err; return err;
if (!tx_buf_.empty()) { }
if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_(); err = try_send_tx_buf_();
if (err != APIError::OK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
} }
} }
return APIError::OK; return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -270,8 +310,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header // read header
if (rx_header_buf_len_ < 3) { if (rx_header_buf_len_ < 3) {
// no header information yet // no header information yet
size_t to_read = 3 - rx_header_buf_len_; uint8_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -284,8 +324,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_header_buf_len_ += received; rx_header_buf_len_ += static_cast<uint8_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint8_t>(received) != to_read) {
// not a full read // not a full read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -317,8 +357,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < msg_size) { if (rx_buf_len_ < msg_size) {
// more data to read // more data to read
size_t to_read = msg_size - rx_buf_len_; uint16_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -331,8 +371,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += received; rx_buf_len_ += static_cast<uint16_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint16_t>(received) != to_read) {
// not all read // not all read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -381,6 +421,8 @@ APIError APINoiseFrameHelper::state_action_() {
if (aerr != APIError::OK) if (aerr != APIError::OK)
return aerr; return aerr;
// ignore contents, may be used in future for flags // ignore contents, may be used in future for flags
// Reserve space for: existing prologue + 2 size bytes + frame data
prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
prologue_.push_back((uint8_t) (frame.msg.size() >> 8)); prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size()); prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
@ -389,16 +431,20 @@ APIError APINoiseFrameHelper::state_action_() {
} }
if (state_ == State::SERVER_HELLO) { if (state_ == State::SERVER_HELLO) {
// send server hello // send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg; std::vector<uint8_t> msg;
// Reserve space for: 1 byte proto + name + null + mac + null
msg.reserve(1 + name.size() + 1 + mac.size() + 1);
// chosen proto // chosen proto
msg.push_back(0x01); msg.push_back(0x01);
// node name, terminated by null byte // node name, terminated by null byte
const std::string &name = App.get_name();
const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str()); const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1); msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
// node mac, terminated by null byte // node mac, terminated by null byte
const std::string &mac = get_mac_address();
const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str()); const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1); msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
@ -493,16 +539,18 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
std::vector<uint8_t> data; std::vector<uint8_t> data;
data.resize(reason.length() + 1); data.resize(reason.length() + 1);
data[0] = 0x01; // failure data[0] = 0x01; // failure
for (size_t i = 0; i < reason.length(); i++) {
data[i + 1] = (uint8_t) reason[i]; // Copy error message in bulk
if (!reason.empty()) {
std::memcpy(data.data() + 1, reason.c_str(), reason.length());
} }
// temporarily remove failed state // temporarily remove failed state
auto orig_state = state_; auto orig_state = state_;
state_ = State::EXPLICIT_REJECT; state_ = State::EXPLICIT_REJECT;
write_frame_(data.data(), data.size()); write_frame_(data.data(), data.size());
state_ = orig_state; state_ = orig_state;
} }
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err; int err;
APIError aerr; APIError aerr;
@ -530,7 +578,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::CIPHERSTATE_DECRYPT_FAILED; return APIError::CIPHERSTATE_DECRYPT_FAILED;
} }
size_t msg_size = mbuf.size; uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data(); uint8_t *msg_data = frame.msg.data();
if (msg_size < 4) { if (msg_size < 4) {
state_ = State::FAILED; state_ = State::FAILED;
@ -556,8 +604,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type; buffer->type = type;
return APIError::OK; return APIError::OK;
} }
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
int err; int err;
APIError aerr; APIError aerr;
aerr = state_action_(); aerr = state_action_();
@ -569,31 +616,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
size_t padding = 0; std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
size_t msg_len = 4 + payload_len + padding; // Message data starts after padding
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); uint16_t payload_len = raw_buffer->size() - frame_header_padding_;
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]}; uint16_t padding = 0;
if (tmpbuf == nullptr) { uint16_t msg_len = 4 + payload_len + padding;
HELPER_LOG("Could not allocate for writing packet");
return APIError::OUT_OF_MEMORY;
}
tmpbuf[0] = 0x01; // indicator // We need to resize to include MAC space, but we already reserved it in create_buffer
// tmpbuf[1], tmpbuf[2] to be set later raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
// Write the noise header in the padded area
// Buffer layout:
// [0] - 0x01 indicator byte
// [1-2] - Size of encrypted payload (filled after encryption)
// [3-4] - Message type (encrypted)
// [5-6] - Payload length (encrypted)
// [7...] - Actual payload data (encrypted)
uint8_t *buf_start = raw_buffer->data();
buf_start[0] = 0x01; // indicator
// buf_start[1], buf_start[2] to be set later after encryption
const uint8_t msg_offset = 3; const uint8_t msg_offset = 3;
const uint8_t payload_offset = msg_offset + 4; buf_start[msg_offset + 0] = (uint8_t) (type >> 8); // type high byte
tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8); // type buf_start[msg_offset + 1] = (uint8_t) type; // type low byte
tmpbuf[msg_offset + 1] = (uint8_t) type; buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len high byte
tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8); // data_len buf_start[msg_offset + 3] = (uint8_t) payload_len; // data_len low byte
tmpbuf[msg_offset + 3] = (uint8_t) payload_len; // payload data is already in the buffer starting at position 7
// copy data
std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]);
// fill padding with zeros
std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0);
NoiseBuffer mbuf; NoiseBuffer mbuf;
noise_buffer_init(mbuf); noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); // The capacity parameter should be msg_len + frame_footer_size_ (MAC length) to allow space for encryption
noise_buffer_set_inout(mbuf, buf_start + msg_offset, msg_len, msg_len + frame_footer_size_);
err = noise_cipherstate_encrypt(send_cipher_, &mbuf); err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
if (err != 0) { if (err != 0) {
state_ = State::FAILED; state_ = State::FAILED;
@ -601,38 +653,20 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::CIPHERSTATE_ENCRYPT_FAILED; return APIError::CIPHERSTATE_ENCRYPT_FAILED;
} }
size_t total_len = 3 + mbuf.size; uint16_t total_len = 3 + mbuf.size;
tmpbuf[1] = (uint8_t) (mbuf.size >> 8); buf_start[1] = (uint8_t) (mbuf.size >> 8);
tmpbuf[2] = (uint8_t) mbuf.size; buf_start[2] = (uint8_t) mbuf.size;
struct iovec iov; struct iovec iov;
iov.iov_base = &tmpbuf[0]; // Point iov_base to the beginning of the buffer (no unused padding in Noise)
// We send the entire frame: indicator + size + encrypted(type + data_len + payload + MAC)
iov.iov_base = buf_start;
iov.iov_len = total_len; iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled // write raw to not have two packets sent if NAGLE disabled
return write_raw_(&iov, 1); return this->write_raw_(&iov, 1);
} }
APIError APINoiseFrameHelper::try_send_tx_buf_() { APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
uint8_t header[3]; uint8_t header[3];
header[0] = 0x01; // indicator header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8); header[1] = (uint8_t) (len >> 8);
@ -642,12 +676,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
iov[0].iov_base = header; iov[0].iov_base = header;
iov[0].iov_len = 3; iov[0].iov_len = 3;
if (len == 0) { if (len == 0) {
return write_raw_(iov, 1); return this->write_raw_(iov, 1);
} }
iov[1].iov_base = const_cast<uint8_t *>(data); iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len; iov[1].iov_len = len;
return write_raw_(iov, 2); return this->write_raw_(iov, 2);
} }
/** Initiate the data structures for the handshake. /** Initiate the data structures for the handshake.
@ -718,6 +752,8 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
return APIError::HANDSHAKESTATE_SPLIT_FAILED; return APIError::HANDSHAKESTATE_SPLIT_FAILED;
} }
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
HELPER_LOG("Handshake complete!"); HELPER_LOG("Handshake complete!");
noise_handshakestate_free(handshake_); noise_handshakestate_free(handshake_);
handshake_ = nullptr; handshake_ = nullptr;
@ -740,22 +776,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
} }
} }
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" { extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system) // declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) { void noise_rand_bytes(void *output, size_t len) {
@ -766,32 +786,15 @@ void noise_rand_bytes(void *output, size_t len) {
} }
} }
// Explicit template instantiation for Noise
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful. /// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() { APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) { APIError err = init_common_();
HELPER_LOG("Bad state for init %d", (int) state_); if (err != APIError::OK) {
return APIError::BAD_STATE; return err;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
} }
state_ = State::DATA; state_ = State::DATA;
@ -802,14 +805,13 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
// try send pending TX data if (!this->tx_buf_.empty()) {
if (!tx_buf_.empty()) {
APIError err = try_send_tx_buf_(); APIError err = try_send_tx_buf_();
if (err != APIError::OK) { if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err; return err;
} }
} }
return APIError::OK; return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
} }
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@ -830,7 +832,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header // read header
while (!rx_header_parsed_) { while (!rx_header_parsed_) {
uint8_t data; uint8_t data;
ssize_t received = socket_->read(&data, 1); // Reading one byte at a time is fastest in practice for ESP32 when
// there is no data on the wire (which is the common case).
// This results in faster failure detection compared to
// attempting to read multiple bytes at once.
ssize_t received = this->socket_->read(&data, 1);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -843,32 +849,77 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_header_buf_.push_back(data);
// try parse header // Successfully read a byte
if (rx_header_buf_[0] != 0x00) {
state_ = State::FAILED; // Process byte according to current buffer position
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); if (rx_header_buf_pos_ == 0) { // Case 1: First byte (indicator byte)
return APIError::BAD_INDICATOR; if (data != 0x00) {
state_ = State::FAILED;
HELPER_LOG("Bad indicator byte %u", data);
return APIError::BAD_INDICATOR;
}
// We don't store the indicator byte, just increment position
rx_header_buf_pos_ = 1; // Set to 1 directly
continue; // Need more bytes before we can parse
} }
size_t i = 1; // Check buffer overflow before storing
if (rx_header_buf_pos_ == 5) { // Case 2: Buffer would overflow (5 bytes is max allowed)
state_ = State::FAILED;
HELPER_LOG("Header buffer overflow");
return APIError::BAD_DATA_PACKET;
}
// Store byte in buffer (adjust index to account for skipped indicator byte)
rx_header_buf_[rx_header_buf_pos_ - 1] = data;
// Increment position after storing
rx_header_buf_pos_++;
// Case 3: If we only have one varint byte, we need more
if (rx_header_buf_pos_ == 2) { // Have read indicator + 1 byte
continue; // Need more bytes before we can parse
}
// At this point, we have at least 3 bytes total:
// - Validated indicator byte (0x00) but not stored
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// First 1-3 bytes: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// Remaining 1-2 bytes: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
// we'll continue reading more bytes.
uint32_t consumed = 0; uint32_t consumed = 0;
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[0], rx_header_buf_pos_ - 1, &consumed);
if (!msg_size_varint.has_value()) { if (!msg_size_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
i += consumed; if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
rx_header_parsed_len_ = msg_size_varint->as_uint32(); state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) { if (!msg_type_varint.has_value()) {
// not enough data there yet // not enough data there yet
continue; continue;
} }
rx_header_parsed_type_ = msg_type_varint->as_uint32(); if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_ = true; rx_header_parsed_ = true;
} }
// header reading done // header reading done
@ -880,8 +931,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
if (rx_buf_len_ < rx_header_parsed_len_) { if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read // more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_; uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
@ -894,8 +945,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed"); HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED; return APIError::CONNECTION_CLOSED;
} }
rx_buf_len_ += received; rx_buf_len_ += static_cast<uint16_t>(received);
if ((size_t) received != to_read) { if (static_cast<uint16_t>(received) != to_read) {
// not all read // not all read
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
@ -909,11 +960,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// consume msg // consume msg
rx_buf_ = {}; rx_buf_ = {};
rx_buf_len_ = 0; rx_buf_len_ = 0;
rx_header_buf_.clear(); rx_header_buf_pos_ = 0;
rx_header_parsed_ = false; rx_header_parsed_ = false;
return APIError::OK; return APIError::OK;
} }
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr; APIError aerr;
@ -941,7 +991,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte"; "Bad indicator byte";
iov[0].iov_base = (void *) msg; iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19; iov[0].iov_len = 19;
write_raw_(iov, 1); this->write_raw_(iov, 1);
} }
return aerr; return aerr;
} }
@ -952,70 +1002,68 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_; buffer->type = rx_header_parsed_type_;
return APIError::OK; return APIError::OK;
} }
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) {
if (state_ != State::DATA) { if (state_ != State::DATA) {
return APIError::BAD_STATE; return APIError::BAD_STATE;
} }
std::vector<uint8_t> header; std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
header.reserve(1 + api::ProtoSize::varint(static_cast<uint32_t>(payload_len)) + // Message data starts after padding (frame_header_padding_ = 6)
api::ProtoSize::varint(static_cast<uint32_t>(type))); uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
header.push_back(0x00);
ProtoVarInt(payload_len).encode(header);
ProtoVarInt(type).encode(header);
struct iovec iov[2]; // Calculate varint sizes for header components
iov[0].iov_base = &header[0]; uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
iov[0].iov_len = header.size(); uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
if (payload_len == 0) { uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
return write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2); if (total_header_len > frame_header_padding_) {
} // Header is too large to fit in the padding
APIError APIPlaintextFrameHelper::try_send_tx_buf_() { return APIError::BAD_ARG;
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
} }
return APIError::OK; // Calculate where to start writing the header
// The header starts at the latest possible position to minimize unused padding
//
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
// [0-2] - Unused padding
// [3] - 0x00 indicator byte
// [4] - Payload size varint (1 byte, for sizes 0-127)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
// [0-1] - Unused padding
// [2] - 0x00 indicator byte
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
// [5] - Message type varint (1 byte, for types 0-127)
// [6...] - Actual payload data
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
uint8_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode size varint directly into buffer
ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
// Encode type varint directly into buffer
ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
struct iovec iov;
// Point iov_base to the beginning of our header (skip unused padding)
// This ensures we only send the actual header and payload, not the empty padding bytes
iov.iov_base = buf_start + header_offset;
iov.iov_len = total_header_len + payload_len;
return write_raw_(&iov, 1);
} }
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Explicit template instantiation for Plaintext
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
#endif // USE_API_PLAINTEXT #endif // USE_API_PLAINTEXT
} // namespace api } // namespace api

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <deque> #include <deque>
#include <limits>
#include <utility> #include <utility>
#include <vector> #include <vector>
@ -16,18 +17,13 @@
namespace esphome { namespace esphome {
namespace api { namespace api {
class ProtoWriteBuffer;
struct ReadPacketBuffer { struct ReadPacketBuffer {
std::vector<uint8_t> container; std::vector<uint8_t> container;
uint16_t type; uint16_t type;
size_t data_offset; uint16_t data_offset;
size_t data_len; uint16_t data_len;
};
struct PacketBuffer {
const std::vector<uint8_t> container;
uint16_t type;
uint8_t data_offset;
uint8_t data_len;
}; };
enum class APIError : int { enum class APIError : int {
@ -60,71 +56,147 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper { class APIFrameHelper {
public: public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
socket_ = socket_owned_.get();
}
virtual ~APIFrameHelper() = default; virtual ~APIFrameHelper() = default;
virtual APIError init() = 0; virtual APIError init() = 0;
virtual APIError loop() = 0; virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0; virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0; bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; std::string getpeername() { return socket_->getpeername(); }
virtual std::string getpeername() = 0; int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; APIError close() {
virtual APIError close() = 0; state_ = State::CLOSED;
virtual APIError shutdown(int how) = 0; int err = this->socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError shutdown(int how) {
int err = this->socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Give this helper a name for logging // Give this helper a name for logging
virtual void set_log_info(std::string info) = 0; void set_log_info(std::string info) { info_ = std::move(info); }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
protected: protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent
struct SendBuffer {
std::vector<uint8_t> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket // Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
template<typename StateEnum> template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state); const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// Common initialization for both plaintext and noise protocols
APIError init_common_();
}; };
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper { class APINoiseFrameHelper : public APIFrameHelper {
public: public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx) APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {} : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
// Pos 7+: actual payload data
frame_header_padding_ = 7;
}
~APINoiseFrameHelper() override; ~APINoiseFrameHelper() override;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; // Get the frame header padding required by this protocol
std::string getpeername() override { return this->socket_->getpeername(); } uint8_t frame_header_padding() override { return frame_header_padding_; }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { // Get the frame footer size required by this protocol
return this->socket_->getpeername(addr, addrlen); uint8_t frame_footer_size() override { return frame_footer_size_; }
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected: protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_(); APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError write_frame_(const uint8_t *data, uint16_t len);
APIError write_frame_(const uint8_t *data, size_t len);
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
APIError init_handshake_(); APIError init_handshake_();
APIError check_handshake_finished_(); APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason); void send_explicit_handshake_reject_(const std::string &reason);
// Fixed-size header buffer for noise protocol:
std::unique_ptr<socket::Socket> socket_; // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
std::string info_;
uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0; uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_; std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_; std::shared_ptr<APINoiseContext> ctx_;
@ -132,69 +204,44 @@ class APINoiseFrameHelper : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr}; NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr}; NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_; NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
}; };
#endif // USE_API_NOISE #endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT #ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper { class APIPlaintextFrameHelper : public APIFrameHelper {
public: public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {} APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
// Pos 4-5: message type varint (up to 2 bytes)
// Pos 6+: actual payload data
frame_header_padding_ = 6;
}
~APIPlaintextFrameHelper() override = default; ~APIPlaintextFrameHelper() override = default;
APIError init() override; APIError init() override;
APIError loop() override; APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override; APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; uint8_t frame_header_padding() override { return frame_header_padding_; }
std::string getpeername() override { return this->socket_->getpeername(); } // Get the frame footer size required by this protocol
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { uint8_t frame_footer_size() override { return frame_footer_size_; }
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
protected: protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); // Fixed-size header buffer for plaintext protocol:
inline APIError write_raw_(const struct iovec *iov, int iovcnt) { // We only need space for the two varints since we validate the indicator byte separately.
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED); // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
} // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
std::unique_ptr<socket::Socket> socket_; // While varints could theoretically be up to 10 bytes each for 64-bit values,
// attempting to process messages with headers that large would likely crash the
std::string info_; // ESP32 due to memory constraints.
std::vector<uint8_t> rx_header_buf_; uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false; bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0; uint16_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0; uint16_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
}; };
#endif #endif

View File

@ -96,6 +96,8 @@ template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode v
return "COLOR_MODE_UNKNOWN"; return "COLOR_MODE_UNKNOWN";
case enums::COLOR_MODE_ON_OFF: case enums::COLOR_MODE_ON_OFF:
return "COLOR_MODE_ON_OFF"; return "COLOR_MODE_ON_OFF";
case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
return "COLOR_MODE_LEGACY_BRIGHTNESS";
case enums::COLOR_MODE_BRIGHTNESS: case enums::COLOR_MODE_BRIGHTNESS:
return "COLOR_MODE_BRIGHTNESS"; return "COLOR_MODE_BRIGHTNESS";
case enums::COLOR_MODE_WHITE: case enums::COLOR_MODE_WHITE:

View File

@ -41,7 +41,8 @@ enum FanDirection : uint32_t {
enum ColorMode : uint32_t { enum ColorMode : uint32_t {
COLOR_MODE_UNKNOWN = 0, COLOR_MODE_UNKNOWN = 0,
COLOR_MODE_ON_OFF = 1, COLOR_MODE_ON_OFF = 1,
COLOR_MODE_BRIGHTNESS = 2, COLOR_MODE_LEGACY_BRIGHTNESS = 2,
COLOR_MODE_BRIGHTNESS = 3,
COLOR_MODE_WHITE = 7, COLOR_MODE_WHITE = 7,
COLOR_MODE_COLOR_TEMPERATURE = 11, COLOR_MODE_COLOR_TEMPERATURE = 11,
COLOR_MODE_COLD_WARM_WHITE = 19, COLOR_MODE_COLD_WARM_WHITE = 19,

View File

@ -20,16 +20,26 @@ class ProtoVarInt {
explicit ProtoVarInt(uint64_t value) : value_(value) {} explicit ProtoVarInt(uint64_t value) : value_(value) {}
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) { static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
if (consumed != nullptr) if (len == 0) {
*consumed = 0; if (consumed != nullptr)
*consumed = 0;
if (len == 0)
return {}; return {};
}
uint64_t result = 0; // Most common case: single-byte varint (values 0-127)
uint8_t bitpos = 0; if ((buffer[0] & 0x80) == 0) {
if (consumed != nullptr)
*consumed = 1;
return ProtoVarInt(buffer[0]);
}
for (uint32_t i = 0; i < len; i++) { // General case for multi-byte varints
// Since we know buffer[0]'s high bit is set, initialize with its value
uint64_t result = buffer[0] & 0x7F;
uint8_t bitpos = 7;
// Start from the second byte since we've already processed the first
for (uint32_t i = 1; i < len; i++) {
uint8_t val = buffer[i]; uint8_t val = buffer[i];
result |= uint64_t(val & 0x7F) << uint64_t(bitpos); result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
bitpos += 7; bitpos += 7;
@ -40,9 +50,12 @@ class ProtoVarInt {
} }
} }
return {}; if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
} }
uint16_t as_uint16() const { return this->value_; }
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; } uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; } bool as_bool() const { return this->value_; }
@ -71,6 +84,34 @@ class ProtoVarInt {
return static_cast<int64_t>(this->value_ >> 1); return static_cast<int64_t>(this->value_ >> 1);
} }
} }
/**
* Encode the varint value to a pre-allocated buffer without bounds checking.
*
* @param buffer The pre-allocated buffer to write the encoded varint to
* @param len The size of the buffer in bytes
*
* @note The caller is responsible for ensuring the buffer is large enough
* to hold the encoded value. Use ProtoSize::varint() to calculate
* the exact size needed before calling this method.
* @note No bounds checking is performed for performance reasons.
*/
void encode_to_buffer_unchecked(uint8_t *buffer, size_t len) {
uint64_t val = this->value_;
if (val <= 0x7F) {
buffer[0] = val;
return;
}
size_t i = 0;
while (val && i < len) {
uint8_t temp = val & 0x7F;
val >>= 7;
if (val) {
buffer[i++] = temp | 0x80;
} else {
buffer[i++] = temp;
}
}
}
void encode(std::vector<uint8_t> &out) { void encode(std::vector<uint8_t> &out) {
uint64_t val = this->value_; uint64_t val = this->value_;
if (val <= 0x7F) { if (val <= 0x7F) {

View File

@ -14,11 +14,8 @@ namespace esphome {
namespace at581x { namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice { class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public: public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) { void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s; this->rf_power_switch_ = s;
s->turn_on(); s->turn_on();
@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data); bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected: protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_; int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */ int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */ int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@bazuchan"] CODEOWNERS = ["@bazuchan"]
@ -9,13 +7,8 @@ CODEOWNERS = ["@bazuchan"]
ballu_ns = cg.esphome_ns.namespace("ballu") ballu_ns = cg.esphome_ns.namespace("ballu")
BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR) BalluClimate = ballu_ns.class_("BalluClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(BalluClimate)
{
cv.GenerateID(): cv.declare_id(BalluClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -9,7 +9,6 @@ from esphome.const import (
CONF_DEFAULT_TARGET_TEMPERATURE_LOW, CONF_DEFAULT_TARGET_TEMPERATURE_LOW,
CONF_HEAT_ACTION, CONF_HEAT_ACTION,
CONF_HUMIDITY_SENSOR, CONF_HUMIDITY_SENSOR,
CONF_ID,
CONF_IDLE_ACTION, CONF_IDLE_ACTION,
CONF_SENSOR, CONF_SENSOR,
) )
@ -19,9 +18,9 @@ BangBangClimate = bang_bang_ns.class_("BangBangClimate", climate.Climate, cg.Com
BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig") BangBangClimateTargetTempConfig = bang_bang_ns.struct("BangBangClimateTargetTempConfig")
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(BangBangClimate)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BangBangClimate),
cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor), cv.Optional(CONF_HUMIDITY_SENSOR): cv.use_id(sensor.Sensor),
cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature, cv.Required(CONF_DEFAULT_TARGET_TEMPERATURE_LOW): cv.temperature,
@ -36,15 +35,15 @@ CONFIG_SCHEMA = cv.All(
} }
), ),
} }
).extend(cv.COMPONENT_SCHEMA), )
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION), cv.has_at_least_one_key(CONF_COOL_ACTION, CONF_HEAT_ACTION),
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
sens = await cg.get_variable(config[CONF_SENSOR]) sens = await cg.get_variable(config[CONF_SENSOR])
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))

View File

@ -3,6 +3,7 @@
#include "bedjet_hub.h" #include "bedjet_hub.h"
#include "bedjet_child.h" #include "bedjet_child.h"
#include "bedjet_const.h" #include "bedjet_const.h"
#include "esphome/core/application.h"
#include <cinttypes> #include <cinttypes>
namespace esphome { namespace esphome {

View File

@ -1,11 +1,8 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, climate from esphome.components import ble_client, climate
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_HEAT_MODE, CONF_HEAT_MODE,
CONF_ID,
CONF_RECEIVE_TIMEOUT, CONF_RECEIVE_TIMEOUT,
CONF_TEMPERATURE_SOURCE, CONF_TEMPERATURE_SOURCE,
CONF_TIME_ID, CONF_TIME_ID,
@ -13,7 +10,6 @@ from esphome.const import (
from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child from .. import BEDJET_CLIENT_SCHEMA, bedjet_ns, register_bedjet_child
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@jhansche"] CODEOWNERS = ["@jhansche"]
DEPENDENCIES = ["bedjet"] DEPENDENCIES = ["bedjet"]
@ -30,9 +26,9 @@ BEDJET_TEMPERATURE_SOURCES = {
} }
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( climate.climate_schema(BedJetClimate)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BedJetClimate),
cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum(
BEDJET_HEAT_MODES, lower=True BEDJET_HEAT_MODES, lower=True
), ),
@ -63,9 +59,8 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)
cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) cg.add(var.set_heating_mode(config[CONF_HEAT_MODE]))

View File

@ -15,17 +15,21 @@ void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state)) if (!this->publish_dedup_.next(state))
return; return;
if (this->filter_list_ == nullptr) { if (this->filter_list_ == nullptr) {
this->send_state_internal(state); this->send_state_internal(state, false);
} else { } else {
this->filter_list_->input(state); this->filter_list_->input(state, false);
} }
} }
void BinarySensor::publish_initial_state(bool state) { void BinarySensor::publish_initial_state(bool state) {
this->has_state_ = false; if (!this->publish_dedup_.next(state))
this->publish_state(state); return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
} }
void BinarySensor::send_state_internal(bool state) { void BinarySensor::send_state_internal(bool state, bool is_initial) {
bool is_initial = !this->has_state_;
if (is_initial) { if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state)); ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else { } else {

View File

@ -67,7 +67,7 @@ class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
// ========== INTERNAL METHODS ========== // ========== INTERNAL METHODS ==========
// (In most use cases you won't need these) // (In most use cases you won't need these)
void send_state_internal(bool state); void send_state_internal(bool state, bool is_initial);
/// Return whether this binary sensor has outputted a state. /// Return whether this binary sensor has outputted a state.
virtual bool has_state() const; virtual bool has_state() const;

View File

@ -9,37 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter"; static const char *const TAG = "sensor.filter";
void Filter::output(bool value) { void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value)) if (!this->dedup_.next(value))
return; return;
if (this->next_ == nullptr) { if (this->next_ == nullptr) {
this->parent_->send_state_internal(value); this->parent_->send_state_internal(value, is_initial);
} else { } else {
this->next_->input(value); this->next_->input(value, is_initial);
} }
} }
void Filter::input(bool value) { void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value); auto b = this->new_value(value, is_initial);
if (b.has_value()) { if (b.has_value()) {
this->output(*b); this->output(*b, is_initial);
} }
} }
optional<bool> DelayedOnOffFilter::new_value(bool value) { optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
} else { } else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); }); this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
} }
return {}; return {};
} }
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value) { optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); }); this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("ON"); this->cancel_timeout("ON");
@ -49,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value) { optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) { if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); }); this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
return {}; return {};
} else { } else {
this->cancel_timeout("OFF"); this->cancel_timeout("OFF");
@ -61,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; } float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value) { return !value; } optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {} AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value) { optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
if (value) { if (value) {
// Ignore if already running // Ignore if already running
if (this->active_timing_ != 0) if (this->active_timing_ != 0)
@ -101,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) { void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2]; const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val); this->output(val, false); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); }); this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
} }
@ -109,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {} LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); } optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) { optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) { if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() { this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true; this->steady_ = true;
this->output(value); this->output(value, is_initial);
}); });
return {}; return {};
} else { } else {
this->steady_ = false; this->steady_ = false;
this->output(value); this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; }); this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value; return value;
} }

View File

@ -14,11 +14,11 @@ class BinarySensor;
class Filter { class Filter {
public: public:
virtual optional<bool> new_value(bool value) = 0; virtual optional<bool> new_value(bool value, bool is_initial) = 0;
void input(bool value); void input(bool value, bool is_initial);
void output(bool value); void output(bool value, bool is_initial);
protected: protected:
friend BinarySensor; friend BinarySensor;
@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component { class DelayedOnOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component { class DelayedOnFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component { class DelayedOffFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter { class InvertFilter : public Filter {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
}; };
struct AutorepeatFilterTiming { struct AutorepeatFilterTiming {
@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public: public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings); explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;
@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public: public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f); explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
protected: protected:
std::function<optional<bool>(bool)> f_; std::function<optional<bool>(bool)> f_;
@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component { class SettleFilter : public Filter, public Component {
public: public:
optional<bool> new_value(bool value) override; optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override; float get_setup_priority() const override;

View File

@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID, CONF_ID,
CONF_LINE_FREQUENCY, CONF_LINE_FREQUENCY,
CONF_POWER, CONF_POWER,
CONF_RESET,
CONF_VOLTAGE, CONF_VOLTAGE,
DEVICE_CLASS_CURRENT, DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
@ -27,7 +28,6 @@ from esphome.const import (
CONF_CURRENT_REFERENCE = "current_reference" CONF_CURRENT_REFERENCE = "current_reference"
CONF_ENERGY_REFERENCE = "energy_reference" CONF_ENERGY_REFERENCE = "energy_reference"
CONF_POWER_REFERENCE = "power_reference" CONF_POWER_REFERENCE = "power_reference"
CONF_RESET = "reset"
CONF_VOLTAGE_REFERENCE = "voltage_reference" CONF_VOLTAGE_REFERENCE = "voltage_reference"
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]

View File

@ -2,6 +2,7 @@
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/macros.h" #include "esphome/core/macros.h"
#include "esphome/core/application.h"
#ifdef USE_ESP32 #ifdef USE_ESP32
@ -177,7 +178,7 @@ void BluetoothProxy::loop() {
// Flush any pending BLE advertisements that have been accumulated but not yet sent // Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) { if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0; static uint32_t last_flush_time = 0;
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms // Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) { if (now - last_flush_time >= 100) {

View File

@ -16,7 +16,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
DOMAIN = "bme68x_bsec2" DOMAIN = "bme68x_bsec2"
BSEC2_LIBRARY_VERSION = "v1.8.2610" BSEC2_LIBRARY_VERSION = "1.10.2610"
CONF_ALGORITHM_OUTPUT = "algorithm_output" CONF_ALGORITHM_OUTPUT = "algorithm_output"
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id" CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
@ -145,7 +145,6 @@ CONFIG_SCHEMA_BASE = (
): cv.positive_time_period_minutes, ): cv.positive_time_period_minutes,
}, },
) )
.add_extra(cv.only_with_arduino)
.add_extra(validate_bme68x) .add_extra(validate_bme68x)
.add_extra(download_bme68x_blob) .add_extra(download_bme68x_blob)
) )
@ -179,11 +178,13 @@ async def to_code_base(config):
bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs))) cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
# Although this component does not use SPI, the BSEC2 library requires the SPI library # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library
cg.add_library("SPI", None) if core.CORE.using_arduino:
cg.add_library("SPI", None)
cg.add_library( cg.add_library(
"BME68x Sensor library", "BME68x Sensor library",
"1.1.40407", "1.3.40408",
"https://github.com/boschsensortec/Bosch-BME68x-Library",
) )
cg.add_library( cg.add_library(
"BSEC2 Software Library", "BSEC2 Software Library",

View File

@ -1,4 +1,5 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"

View File

@ -1,4 +1,5 @@
#include "esphome/core/defines.h" #include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"

View File

@ -32,14 +32,14 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(CCS811Component), cv.GenerateID(): cv.declare_id(CCS811Component),
cv.Required(CONF_ECO2): sensor.sensor_schema( cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION, unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2, icon=ICON_MOLECULE_CO2,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_TVOC): sensor.sensor_schema( cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION, unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR, icon=ICON_RADIATOR,
accuracy_decimals=0, accuracy_decimals=0,
@ -64,10 +64,13 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2]) if eco2_config := config.get(CONF_ECO2):
cg.add(var.set_co2(sens)) sens = await sensor.new_sensor(eco2_config)
sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_co2(sens))
cg.add(var.set_tvoc(sens))
if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_tvoc(sens))
if version_config := config.get(CONF_VERSION): if version_config := config.get(CONF_VERSION):
sens = await text_sensor.new_text_sensor(version_config) sens = await text_sensor.new_text_sensor(version_config)

View File

@ -1,7 +1,13 @@
import logging
from esphome import core
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate, remote_base, sensor from esphome.components import climate, remote_base, sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
from esphome.cpp_generator import MockObjClass
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["remote_transmitter"] DEPENDENCIES = ["remote_transmitter"]
AUTO_LOAD = ["sensor", "remote_base"] AUTO_LOAD = ["sensor", "remote_base"]
@ -16,30 +22,58 @@ ClimateIR = climate_ir_ns.class_(
remote_base.RemoteTransmittable, remote_base.RemoteTransmittable,
) )
CLIMATE_IR_SCHEMA = (
climate.CLIMATE_SCHEMA.extend( def climate_ir_schema(
class_: MockObjClass,
) -> cv.Schema:
return (
climate.climate_schema(class_)
.extend(
{
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean,
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean,
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
def climate_ir_with_receiver_schema(
class_: MockObjClass,
) -> cv.Schema:
return climate_ir_schema(class_).extend(
{ {
cv.Optional(CONF_SUPPORTS_COOL, default=True): cv.boolean, cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id(
cv.Optional(CONF_SUPPORTS_HEAT, default=True): cv.boolean, remote_base.RemoteReceiverBase
cv.Optional(CONF_SENSOR): cv.use_id(sensor.Sensor), ),
} }
) )
.extend(cv.COMPONENT_SCHEMA)
.extend(remote_base.REMOTE_TRANSMITTABLE_SCHEMA)
)
CLIMATE_IR_WITH_RECEIVER_SCHEMA = CLIMATE_IR_SCHEMA.extend(
{ # Remove before 2025.11.0
cv.Optional(remote_base.CONF_RECEIVER_ID): cv.use_id( def deprecated_schema_constant(config):
remote_base.RemoteReceiverBase type: str = "unknown"
), if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
} type = str(id.type).split("::", maxsplit=1)[0]
) _LOGGER.warning(
"Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
"Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
"If you are seeing this, report an issue to the external_component author and ask them to update it. "
"https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
"Component using this schema: %s",
type,
)
return config
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
async def register_climate_ir(var, config): async def register_climate_ir(var, config):
await cg.register_component(var, config) await cg.register_component(var, config)
await climate.register_climate(var, config)
await remote_base.register_transmittable(var, config) await remote_base.register_transmittable(var, config)
cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL])) cg.add(var.set_supports_cool(config[CONF_SUPPORTS_COOL]))
cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT])) cg.add(var.set_supports_heat(config[CONF_SUPPORTS_HEAT]))
@ -48,3 +82,9 @@ async def register_climate_ir(var, config):
if sensor_id := config.get(CONF_SENSOR): if sensor_id := config.get(CONF_SENSOR):
sens = await cg.get_variable(sensor_id) sens = await cg.get_variable(sensor_id)
cg.add(var.set_sensor(sens)) cg.add(var.set_sensor(sens))
async def new_climate_ir(config, *args):
var = await climate.new_climate(config, *args)
await register_climate_ir(var, config)
return var

View File

@ -1,7 +1,6 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
@ -14,9 +13,8 @@ CONF_BIT_HIGH = "bit_high"
CONF_BIT_ONE_LOW = "bit_one_low" CONF_BIT_ONE_LOW = "bit_one_low"
CONF_BIT_ZERO_LOW = "bit_zero_low" CONF_BIT_ZERO_LOW = "bit_zero_low"
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(LgIrClimate),
cv.Optional( cv.Optional(
CONF_HEADER_HIGH, default="8000us" CONF_HEADER_HIGH, default="8000us"
): cv.positive_time_period_microseconds, ): cv.positive_time_period_microseconds,
@ -37,8 +35,7 @@ CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)
cg.add(var.set_header_high(config[CONF_HEADER_HIGH])) cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
cg.add(var.set_header_low(config[CONF_HEADER_LOW])) cg.add(var.set_header_low(config[CONF_HEADER_LOW]))

View File

@ -0,0 +1 @@
"""CM1106 component for ESPHome."""

View File

@ -0,0 +1,112 @@
#include "cm1106.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace cm1106 {
static const char *const TAG = "cm1106";
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (int i = 0; i < len - 1; i++) {
crc -= response[i];
}
return crc;
}
void CM1106Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up CM1106...");
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
this->mark_failed();
return;
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X...", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
void CM1106Component::calibrate_zero(uint16_t ppm) {
uint8_t cmd[6];
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
size_t response_len) {
// Empty RX Buffer
while (this->available())
this->read();
this->write_array(command, command_len - 1);
this->write_byte(cm1106_checksum(command, command_len));
this->flush();
if (response == nullptr)
return true;
return this->read_array(response, response_len);
}
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
}
}
} // namespace cm1106
} // namespace esphome

View File

@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace cm1106 {
class CM1106Component : public PollingComponent, public uart::UARTDevice {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void setup() override;
void update() override;
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
};
template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> {
public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected:
CM1106Component *cm1106_;
};
} // namespace cm1106
} // namespace esphome

View File

@ -0,0 +1,72 @@
"""CM1106 Sensor component for ESPHome."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import sensor, uart
from esphome.const import (
CONF_CO2,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CM1106CalibrateZeroAction = cm1106_ns.class_(
"CM1106CalibrateZeroAction",
automation.Action,
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CM1106Component),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
},
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config) -> None:
"""Code generation entry point."""
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if co2_config := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2_config)
cg.add(var.set_co2_sensor(sens))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(CM1106Component),
},
)
@automation.register_action(
"cm1106.calibrate_zero",
CM1106CalibrateZeroAction,
CALIBRATION_ACTION_SCHEMA,
)
async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None:
"""Service code generation entry point."""
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@glmnet"] CODEOWNERS = ["@glmnet"]
@ -9,13 +7,8 @@ CODEOWNERS = ["@glmnet"]
coolix_ns = cg.esphome_ns.namespace("coolix") coolix_ns = cg.esphome_ns.namespace("coolix")
CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR) CoolixClimate = coolix_ns.class_("CoolixClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(CoolixClimate)
{
cv.GenerateID(): cv.declare_id(CoolixClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -1,5 +1,6 @@
#include "cse7766.h" #include "cse7766.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace cse7766 { namespace cse7766 {
@ -7,7 +8,7 @@ namespace cse7766 {
static const char *const TAG = "cse7766"; static const char *const TAG = "cse7766";
void CSE7766Component::loop() { void CSE7766Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) { if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
this->raw_data_index_ = 0; this->raw_data_index_ = 0;

View File

@ -1,6 +1,7 @@
#include "current_based_cover.h" #include "current_based_cover.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cfloat> #include <cfloat>
namespace esphome { namespace esphome {
@ -60,7 +61,7 @@ void CurrentBasedCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING) { if (this->current_operation == COVER_OPERATION_OPENING) {
if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction if (this->malfunction_detection_ && this->is_closing_()) { // Malfunction

View File

@ -1,20 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
daikin_ns = cg.esphome_ns.namespace("daikin") daikin_ns = cg.esphome_ns.namespace("daikin")
DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR) DaikinClimate = daikin_ns.class_("DaikinClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinClimate)
{
cv.GenerateID(): cv.declare_id(DaikinClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -1,18 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc") daikin_arc_ns = cg.esphome_ns.namespace("daikin_arc")
DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR) DaikinArcClimate = daikin_arc_ns.class_("DaikinArcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinArcClimate)
{cv.GenerateID(): cv.declare_id(DaikinArcClimate)}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_USE_FAHRENHEIT from esphome.const import CONF_USE_FAHRENHEIT
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
@ -9,15 +9,13 @@ daikin_brc_ns = cg.esphome_ns.namespace("daikin_brc")
DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR) DaikinBrcClimate = daikin_brc_ns.class_("DaikinBrcClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DaikinBrcClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(DaikinBrcClimate),
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean, cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
} }
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)
cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT])) cg.add(var.set_fahrenheit(config[CONF_USE_FAHRENHEIT]))

View File

@ -1,6 +1,7 @@
#include "daly_bms.h" #include "daly_bms.h"
#include <vector> #include <vector>
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace daly_bms { namespace daly_bms {
@ -32,7 +33,7 @@ void DalyBmsComponent::update() {
} }
void DalyBmsComponent::loop() { void DalyBmsComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->receiving_ && (now - this->last_transmission_ >= 200)) { if (this->receiving_ && (now - this->last_transmission_ >= 200)) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index."); ESP_LOGW(TAG, "Last transmission too long ago. Reset RX index.");

View File

@ -2,7 +2,6 @@ import base64
from pathlib import Path from pathlib import Path
import re import re
import secrets import secrets
from typing import Optional
import requests import requests
from ruamel.yaml import YAML from ruamel.yaml import YAML
@ -84,7 +83,7 @@ async def to_code(config):
def import_config( def import_config(
path: str, path: str,
name: str, name: str,
friendly_name: Optional[str], friendly_name: str | None,
project_name: str, project_name: str,
import_url: str, import_url: str,
network: str = CONF_WIFI, network: str = CONF_WIFI,

View File

@ -70,7 +70,7 @@ void DebugComponent::loop() {
#ifdef USE_SENSOR #ifdef USE_SENSOR
// calculate loop time - from last call to this one // calculate loop time - from last call to this one
if (this->loop_time_sensor_ != nullptr) { if (this->loop_time_sensor_ != nullptr) {
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
uint32_t loop_time = now - this->last_loop_timetag_; uint32_t loop_time = now - this->last_loop_timetag_;
this->max_loop_time_ = std::max(this->max_loop_time_, loop_time); this->max_loop_time_ = std::max(this->max_loop_time_, loop_time);
this->last_loop_timetag_ = now; this->last_loop_timetag_ = now;

View File

@ -34,13 +34,15 @@ class DebugComponent : public PollingComponent {
#endif #endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; } void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32 #ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; } void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32 #endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) { void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor; this->cpu_frequency_sensor_ = cpu_frequency_sensor;
} }
#endif // USE_SENSOR #endif // USE_SENSOR
#ifdef USE_ESP32
void on_shutdown() override;
#endif // USE_ESP32
protected: protected:
uint32_t free_heap_{}; uint32_t free_heap_{};

View File

@ -1,20 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
delonghi_ns = cg.esphome_ns.namespace("delonghi") delonghi_ns = cg.esphome_ns.namespace("delonghi")
DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR) DelonghiClimate = delonghi_ns.class_("DelonghiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(DelonghiClimate)
{
cv.GenerateID(): cv.declare_id(DelonghiClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -27,14 +27,14 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(DPS310Component), cv.GenerateID(): cv.declare_id(DPS310Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,
icon=ICON_THERMOMETER, icon=ICON_THERMOMETER,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_PRESSURE): sensor.sensor_schema( cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL, unit_of_measurement=UNIT_HECTOPASCAL,
icon=ICON_GAUGE, icon=ICON_GAUGE,
accuracy_decimals=1, accuracy_decimals=1,
@ -53,10 +53,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_sensor(sens))
if CONF_PRESSURE in config: if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(config[CONF_PRESSURE]) sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_sensor(sens))

View File

@ -26,19 +26,19 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(EE895Component), cv.GenerateID(): cv.declare_id(EE895Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_CO2): sensor.sensor_schema( cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION, unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2, icon=ICON_MOLECULE_CO2,
accuracy_decimals=0, accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_PRESSURE): sensor.sensor_schema( cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
unit_of_measurement=UNIT_HECTOPASCAL, unit_of_measurement=UNIT_HECTOPASCAL,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_PRESSURE, device_class=DEVICE_CLASS_PRESSURE,
@ -56,14 +56,14 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_sensor(sens))
if CONF_CO2 in config: if co2 := config.get(CONF_CO2):
sens = await sensor.new_sensor(config[CONF_CO2]) sens = await sensor.new_sensor(co2)
cg.add(var.set_co2_sensor(sens)) cg.add(var.set_co2_sensor(sens))
if CONF_PRESSURE in config: if pressure := config.get(CONF_PRESSURE):
sens = await sensor.new_sensor(config[CONF_PRESSURE]) sens = await sensor.new_sensor(pressure)
cg.add(var.set_pressure_sensor(sens)) cg.add(var.set_pressure_sensor(sens))

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@E440QF"] CODEOWNERS = ["@E440QF"]
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
@ -9,13 +7,8 @@ AUTO_LOAD = ["climate_ir"]
emmeti_ns = cg.esphome_ns.namespace("emmeti") emmeti_ns = cg.esphome_ns.namespace("emmeti")
EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR) EmmetiClimate = emmeti_ns.class_("EmmetiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(EmmetiClimate)
{
cv.GenerateID(): cv.declare_id(EmmetiClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -1,6 +1,7 @@
#include "endstop_cover.h" #include "endstop_cover.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace endstop { namespace endstop {
@ -65,7 +66,7 @@ void EndstopCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) { if (this->current_operation == COVER_OPERATION_OPENING && this->is_open_()) {
float dur = (now - this->start_dir_time_) / 1e3f; float dur = (now - this->start_dir_time_) / 1e3f;

View File

@ -28,21 +28,21 @@ UNIT_INDEX = "index"
CONFIG_SCHEMA_BASE = cv.Schema( CONFIG_SCHEMA_BASE = cv.Schema(
{ {
cv.Required(CONF_ECO2): sensor.sensor_schema( cv.Optional(CONF_ECO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION, unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2, icon=ICON_MOLECULE_CO2,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE, device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_TVOC): sensor.sensor_schema( cv.Optional(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION, unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR, icon=ICON_RADIATOR,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_AQI): sensor.sensor_schema( cv.Optional(CONF_AQI): sensor.sensor_schema(
icon=ICON_CHEMICAL_WEAPON, icon=ICON_CHEMICAL_WEAPON,
accuracy_decimals=0, accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI, device_class=DEVICE_CLASS_AQI,
@ -62,12 +62,15 @@ async def to_code_base(config):
var = cg.new_Pvariable(config[CONF_ID]) var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config) await cg.register_component(var, config)
sens = await sensor.new_sensor(config[CONF_ECO2]) if eco2_config := config.get(CONF_ECO2):
cg.add(var.set_co2(sens)) sens = await sensor.new_sensor(eco2_config)
sens = await sensor.new_sensor(config[CONF_TVOC]) cg.add(var.set_co2(sens))
cg.add(var.set_tvoc(sens)) if tvoc_config := config.get(CONF_TVOC):
sens = await sensor.new_sensor(config[CONF_AQI]) sens = await sensor.new_sensor(tvoc_config)
cg.add(var.set_aqi(sens)) cg.add(var.set_tvoc(sens))
if aqi_config := config.get(CONF_AQI):
sens = await sensor.new_sensor(aqi_config)
cg.add(var.set_aqi(sens))
if compensation_config := config.get(CONF_COMPENSATION): if compensation_config := config.get(CONF_COMPENSATION):
sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE]) sens = await cg.get_variable(compensation_config[CONF_TEMPERATURE])

View File

@ -3,7 +3,6 @@ import itertools
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional, Union
from esphome import git from esphome import git
import esphome.codegen as cg import esphome.codegen as cg
@ -58,8 +57,10 @@ from .const import ( # noqa
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
VARIANT_FRIENDLY, VARIANT_FRIENDLY,
@ -88,8 +89,10 @@ CPU_FREQUENCIES = {
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240), VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C2: get_cpu_frequencies(80, 120), VARIANT_ESP32C2: get_cpu_frequencies(80, 120),
VARIANT_ESP32C3: get_cpu_frequencies(80, 160), VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160), VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96), VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
} }
# Make sure not missed here if a new variant added. # Make sure not missed here if a new variant added.
@ -189,7 +192,7 @@ class RawSdkconfigValue:
value: str value: str
SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue] SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType): def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
@ -206,8 +209,8 @@ def add_idf_component(
ref: str = None, ref: str = None,
path: str = None, path: str = None,
refresh: TimePeriod = None, refresh: TimePeriod = None,
components: Optional[list[str]] = None, components: list[str] | None = None,
submodules: Optional[list[str]] = None, submodules: list[str] | None = None,
): ):
"""Add an esp-idf component to the project.""" """Add an esp-idf component to the project."""
if not CORE.using_esp_idf: if not CORE.using_esp_idf:
@ -296,11 +299,11 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version # The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases # - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf # - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6) RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2)
# The platformio/espressif32 version to use for esp-idf frameworks # The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases # - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 # - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(51, 3, 7) ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13)
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions # List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [ SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
@ -369,8 +372,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value): def _esp_idf_check_versions(value):
value = value.copy() value = value.copy()
lookups = { lookups = {
"dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"), "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 6), None), "latest": (cv.Version(5, 3, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None), "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
} }

View File

@ -2,8 +2,10 @@ from .const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -1592,6 +1594,10 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit", "name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3, "variant": VARIANT_ESP32C3,
}, },
"esp32-c5-devkitc-1": {
"name": "Espressif ESP32-C5-DevKitC-1",
"variant": VARIANT_ESP32C5,
},
"esp32-c6-devkitc-1": { "esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1", "name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6, "variant": VARIANT_ESP32C6,
@ -1632,6 +1638,14 @@ BOARDS = {
"name": "Espressif ESP32-H2-DevKit", "name": "Espressif ESP32-H2-DevKit",
"variant": VARIANT_ESP32H2, "variant": VARIANT_ESP32H2,
}, },
"esp32-p4": {
"name": "Espressif ESP32-P4 generic",
"variant": VARIANT_ESP32P4,
},
"esp32-p4-evboard": {
"name": "Espressif ESP32-P4 Function EV Board",
"variant": VARIANT_ESP32P4,
},
"esp32-pico-devkitm-2": { "esp32-pico-devkitm-2": {
"name": "Espressif ESP32-PICO-DevKitM-2", "name": "Espressif ESP32-PICO-DevKitM-2",
"variant": VARIANT_ESP32, "variant": VARIANT_ESP32,

View File

@ -17,16 +17,20 @@ VARIANT_ESP32S2 = "ESP32S2"
VARIANT_ESP32S3 = "ESP32S3" VARIANT_ESP32S3 = "ESP32S3"
VARIANT_ESP32C2 = "ESP32C2" VARIANT_ESP32C2 = "ESP32C2"
VARIANT_ESP32C3 = "ESP32C3" VARIANT_ESP32C3 = "ESP32C3"
VARIANT_ESP32C5 = "ESP32C5"
VARIANT_ESP32C6 = "ESP32C6" VARIANT_ESP32C6 = "ESP32C6"
VARIANT_ESP32H2 = "ESP32H2" VARIANT_ESP32H2 = "ESP32H2"
VARIANT_ESP32P4 = "ESP32P4"
VARIANTS = [ VARIANTS = [
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
] ]
VARIANT_FRIENDLY = { VARIANT_FRIENDLY = {
@ -35,8 +39,10 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32S3: "ESP32-S3", VARIANT_ESP32S3: "ESP32-S3",
VARIANT_ESP32C2: "ESP32-C2", VARIANT_ESP32C2: "ESP32-C2",
VARIANT_ESP32C3: "ESP32-C3", VARIANT_ESP32C3: "ESP32-C3",
VARIANT_ESP32C5: "ESP32-C5",
VARIANT_ESP32C6: "ESP32-C6", VARIANT_ESP32C6: "ESP32-C6",
VARIANT_ESP32H2: "ESP32-H2", VARIANT_ESP32H2: "ESP32-H2",
VARIANT_ESP32P4: "ESP32-P4",
} }
esp32_ns = cg.esphome_ns.namespace("esp32") esp32_ns = cg.esphome_ns.namespace("esp32")

View File

@ -15,8 +15,9 @@
#ifdef USE_ARDUINO #ifdef USE_ARDUINO
#include <Esp.h> #include <Esp.h>
#else #else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h> #include <esp_clk_tree.h>
#endif
void setup(); void setup();
void loop(); void loop();
#endif #endif
@ -63,7 +64,13 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0; uint32_t freq = 0;
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO) #elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000; freq = ESP.getCpuFreqMHz() * 1000000;
#endif #endif

View File

@ -1,6 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
import logging import logging
from typing import Any, Callable from typing import Any
from esphome import pins from esphome import pins
import esphome.codegen as cg import esphome.codegen as cg
@ -26,8 +27,10 @@ from .const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
esp32_ns, esp32_ns,
@ -35,8 +38,10 @@ from .const import (
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
@ -97,6 +102,10 @@ _esp32_validations = {
pin_validation=esp32_c3_validate_gpio_pin, pin_validation=esp32_c3_validate_gpio_pin,
usage_validation=esp32_c3_validate_supports, usage_validation=esp32_c3_validate_supports,
), ),
VARIANT_ESP32C5: ESP32ValidationFunctions(
pin_validation=esp32_c5_validate_gpio_pin,
usage_validation=esp32_c5_validate_supports,
),
VARIANT_ESP32C6: ESP32ValidationFunctions( VARIANT_ESP32C6: ESP32ValidationFunctions(
pin_validation=esp32_c6_validate_gpio_pin, pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports, usage_validation=esp32_c6_validate_supports,
@ -105,6 +114,10 @@ _esp32_validations = {
pin_validation=esp32_h2_validate_gpio_pin, pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports, usage_validation=esp32_h2_validate_supports,
), ),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
),
VARIANT_ESP32S2: ESP32ValidationFunctions( VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin, pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports, usage_validation=esp32_s2_validate_supports,

View File

@ -0,0 +1,45 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32C5_SPI_PSRAM_PINS = {
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
}
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
_LOGGER = logging.getLogger(__name__)
def esp32_c5_validate_gpio_pin(value):
if value < 0 or value > 28:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
if value in _ESP32C5_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
)
return value
def esp32_c5_validate_supports(value):
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 28:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
return value

View File

@ -0,0 +1,43 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
_ESP32P4_USB_JTAG_PINS = {24, 25}
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
_LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value):
if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_STRAPPING_PINS:
_LOGGER.warning(
"GPIO%d is a Strapping PIN and should be avoided.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in _ESP32P4_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
"To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
value,
)
return value
def esp32_p4_validate_supports(value):
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if is_input:
# All ESP32 pins support input mode
pass
return value

View File

@ -6,6 +6,7 @@
#include <cstring> #include <cstring>
#include "ble_uuid.h" #include "ble_uuid.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
@ -143,7 +144,7 @@ void BLEAdvertising::loop() {
if (this->raw_advertisements_callbacks_.empty()) { if (this->raw_advertisements_callbacks_.empty()) {
return; return;
} }
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) { if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
this->stop(); this->stop();
this->current_adv_index_ += 1; this->current_adv_index_ += 1;

View File

@ -1,8 +1,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import MutableMapping from collections.abc import Callable, MutableMapping
import logging import logging
from typing import Any, Callable from typing import Any
from esphome import automation from esphome import automation
import esphome.codegen as cg import esphome.codegen as cg

View File

@ -296,7 +296,7 @@ async def to_code(config):
add_idf_component( add_idf_component(
name="esp32-camera", name="esp32-camera",
repo="https://github.com/espressif/esp32-camera.git", repo="https://github.com/espressif/esp32-camera.git",
ref="v2.0.9", ref="v2.0.15",
) )
for conf in config.get(CONF_ON_STREAM_START, []): for conf in config.get(CONF_ON_STREAM_START, []):

View File

@ -3,6 +3,7 @@
#include "esp32_camera.h" #include "esp32_camera.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/application.h"
#include <freertos/task.h> #include <freertos/task.h>
@ -54,11 +55,7 @@ void ESP32Camera::dump_config() {
ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href); ESP_LOGCONFIG(TAG, " HREF Pin: %d", conf.pin_href);
ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk); ESP_LOGCONFIG(TAG, " Pixel Clock Pin: %d", conf.pin_pclk);
ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz); ESP_LOGCONFIG(TAG, " External Clock: Pin:%d Frequency:%u", conf.pin_xclk, conf.xclk_freq_hz);
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sscb_sda, conf.pin_sscb_scl);
#else
ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl); ESP_LOGCONFIG(TAG, " I2C Pins: SDA:%d SCL:%d", conf.pin_sccb_sda, conf.pin_sccb_scl);
#endif
ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset); ESP_LOGCONFIG(TAG, " Reset Pin: %d", conf.pin_reset);
switch (this->config_.frame_size) { switch (this->config_.frame_size) {
case FRAMESIZE_QQVGA: case FRAMESIZE_QQVGA:
@ -162,7 +159,7 @@ void ESP32Camera::loop() {
} }
// request idle image every idle_update_interval // request idle image every idle_update_interval
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) { if (this->idle_update_interval_ != 0 && now - this->last_idle_request_ > this->idle_update_interval_) {
this->last_idle_request_ = now; this->last_idle_request_ = now;
this->request_image(IDLE); this->request_image(IDLE);
@ -238,13 +235,8 @@ void ESP32Camera::set_external_clock(uint8_t pin, uint32_t frequency) {
this->config_.xclk_freq_hz = frequency; this->config_.xclk_freq_hz = frequency;
} }
void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) {
#ifdef USE_ESP_IDF // Temporary until the espressif/esp32-camera library is updated
this->config_.pin_sscb_sda = sda;
this->config_.pin_sscb_scl = scl;
#else
this->config_.pin_sccb_sda = sda; this->config_.pin_sccb_sda = sda;
this->config_.pin_sccb_scl = scl; this->config_.pin_sccb_scl = scl;
#endif
} }
void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; }
void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; }

View File

@ -106,7 +106,7 @@ class CameraImageReader {
}; };
/* ---------------- ESP32Camera class ---------------- */ /* ---------------- ESP32Camera class ---------------- */
class ESP32Camera : public Component, public EntityBase { class ESP32Camera : public EntityBase, public Component {
public: public:
ESP32Camera(); ESP32Camera();

View File

@ -92,7 +92,7 @@ void ESP32ImprovComponent::loop() {
if (!this->incoming_data_.empty()) if (!this->incoming_data_.empty())
this->process_incoming_data_(); this->process_incoming_data_();
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
switch (this->state_) { switch (this->state_) {
case improv::STATE_STOPPED: case improv::STATE_STOPPED:

View File

@ -288,7 +288,7 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
} }
void ESP32TouchComponent::loop() { void ESP32TouchComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250; bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
for (auto *child : this->children_) { for (auto *child : this->children_) {
child->value_ = this->component_touch_pad_read(child->get_touch_pad()); child->value_ = this->component_touch_pad_read(child->get_touch_pad());

View File

@ -111,6 +111,8 @@ void ESPHomeOTAComponent::handle_() {
int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) { if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close();
client_ = nullptr;
return; return;
} }

View File

@ -240,7 +240,7 @@ void EthernetComponent::setup() {
} }
void EthernetComponent::loop() { void EthernetComponent::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) { switch (this->state_) {
case EthernetComponentState::STOPPED: case EthernetComponentState::STOPPED:

View File

@ -1,6 +1,7 @@
#include "feedback_cover.h" #include "feedback_cover.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace feedback { namespace feedback {
@ -220,7 +221,7 @@ void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_o
void FeedbackCover::loop() { void FeedbackCover::loop() {
if (this->current_operation == COVER_OPERATION_IDLE) if (this->current_operation == COVER_OPERATION_IDLE)
return; return;
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
// Recompute position every loop cycle // Recompute position every loop cycle
this->recompute_position_(); this->recompute_position_();

View File

@ -1,7 +1,5 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
@ -10,13 +8,8 @@ FujitsuGeneralClimate = fujitsu_general_ns.class_(
"FujitsuGeneralClimate", climate_ir.ClimateIR "FujitsuGeneralClimate", climate_ir.ClimateIR
) )
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FujitsuGeneralClimate)
{
cv.GenerateID(): cv.declare_id(FujitsuGeneralClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -6,6 +6,7 @@
*/ */
#include "gcja5.h" #include "gcja5.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
#include <cstring> #include <cstring>
namespace esphome { namespace esphome {
@ -16,7 +17,7 @@ static const char *const TAG = "gcja5";
void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); } void GCJA5Component::setup() { ESP_LOGCONFIG(TAG, "Setting up gcja5..."); }
void GCJA5Component::loop() { void GCJA5Component::loop() {
const uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) { if (now - this->last_transmission_ >= 500) {
// last transmission too long ago. Reset RX index. // last transmission too long ago. Reset RX index.
this->rx_message_.clear(); this->rx_message_.clear();

View File

@ -9,23 +9,32 @@ from esphome.const import (
CONF_LONGITUDE, CONF_LONGITUDE,
CONF_SATELLITES, CONF_SATELLITES,
CONF_SPEED, CONF_SPEED,
DEVICE_CLASS_SPEED,
STATE_CLASS_MEASUREMENT, STATE_CLASS_MEASUREMENT,
UNIT_DEGREES, UNIT_DEGREES,
UNIT_KILOMETER_PER_HOUR, UNIT_KILOMETER_PER_HOUR,
UNIT_METER, UNIT_METER,
) )
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
ICON_ALTIMETER = "mdi:altimeter"
ICON_COMPASS = "mdi:compass"
ICON_LATITUDE = "mdi:latitude"
ICON_LONGITUDE = "mdi:longitude"
ICON_SATELLITE = "mdi:satellite-variant"
ICON_SPEEDOMETER = "mdi:speedometer"
DEPENDENCIES = ["uart"] DEPENDENCIES = ["uart"]
AUTO_LOAD = ["sensor"] AUTO_LOAD = ["sensor"]
CODEOWNERS = ["@coogle"] CODEOWNERS = ["@coogle", "@ximex"]
gps_ns = cg.esphome_ns.namespace("gps") gps_ns = cg.esphome_ns.namespace("gps")
GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice) GPS = gps_ns.class_("GPS", cg.Component, uart.UARTDevice)
GPSListener = gps_ns.class_("GPSListener") GPSListener = gps_ns.class_("GPSListener")
CONF_GPS_ID = "gps_id"
CONF_HDOP = "hdop"
MULTI_CONF = True MULTI_CONF = True
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.Schema( cv.Schema(
@ -33,25 +42,37 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(): cv.declare_id(GPS), cv.GenerateID(): cv.declare_id(GPS),
cv.Optional(CONF_LATITUDE): sensor.sensor_schema( cv.Optional(CONF_LATITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES, unit_of_measurement=UNIT_DEGREES,
icon=ICON_LATITUDE,
accuracy_decimals=6, accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_LONGITUDE): sensor.sensor_schema( cv.Optional(CONF_LONGITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES, unit_of_measurement=UNIT_DEGREES,
icon=ICON_LONGITUDE,
accuracy_decimals=6, accuracy_decimals=6,
state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_SPEED): sensor.sensor_schema( cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOMETER_PER_HOUR, unit_of_measurement=UNIT_KILOMETER_PER_HOUR,
icon=ICON_SPEEDOMETER,
accuracy_decimals=3, accuracy_decimals=3,
device_class=DEVICE_CLASS_SPEED,
state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_COURSE): sensor.sensor_schema( cv.Optional(CONF_COURSE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES, unit_of_measurement=UNIT_DEGREES,
icon=ICON_COMPASS,
accuracy_decimals=2, accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_ALTITUDE): sensor.sensor_schema( cv.Optional(CONF_ALTITUDE): sensor.sensor_schema(
unit_of_measurement=UNIT_METER, unit_of_measurement=UNIT_METER,
icon=ICON_ALTIMETER,
accuracy_decimals=2, accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_SATELLITES): sensor.sensor_schema( cv.Optional(CONF_SATELLITES): sensor.sensor_schema(
icon=ICON_SATELLITE,
accuracy_decimals=0, accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
@ -73,28 +94,28 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
if CONF_LATITUDE in config: if latitude_config := config.get(CONF_LATITUDE):
sens = await sensor.new_sensor(config[CONF_LATITUDE]) sens = await sensor.new_sensor(latitude_config)
cg.add(var.set_latitude_sensor(sens)) cg.add(var.set_latitude_sensor(sens))
if CONF_LONGITUDE in config: if longitude_config := config.get(CONF_LONGITUDE):
sens = await sensor.new_sensor(config[CONF_LONGITUDE]) sens = await sensor.new_sensor(longitude_config)
cg.add(var.set_longitude_sensor(sens)) cg.add(var.set_longitude_sensor(sens))
if CONF_SPEED in config: if speed_config := config.get(CONF_SPEED):
sens = await sensor.new_sensor(config[CONF_SPEED]) sens = await sensor.new_sensor(speed_config)
cg.add(var.set_speed_sensor(sens)) cg.add(var.set_speed_sensor(sens))
if CONF_COURSE in config: if course_config := config.get(CONF_COURSE):
sens = await sensor.new_sensor(config[CONF_COURSE]) sens = await sensor.new_sensor(course_config)
cg.add(var.set_course_sensor(sens)) cg.add(var.set_course_sensor(sens))
if CONF_ALTITUDE in config: if altitude_config := config.get(CONF_ALTITUDE):
sens = await sensor.new_sensor(config[CONF_ALTITUDE]) sens = await sensor.new_sensor(altitude_config)
cg.add(var.set_altitude_sensor(sens)) cg.add(var.set_altitude_sensor(sens))
if CONF_SATELLITES in config: if satellites_config := config.get(CONF_SATELLITES):
sens = await sensor.new_sensor(config[CONF_SATELLITES]) sens = await sensor.new_sensor(satellites_config)
cg.add(var.set_satellites_sensor(sens)) cg.add(var.set_satellites_sensor(sens))
if hdop_config := config.get(CONF_HDOP): if hdop_config := config.get(CONF_HDOP):
@ -102,4 +123,4 @@ async def to_code(config):
cg.add(var.set_hdop_sensor(sens)) cg.add(var.set_hdop_sensor(sens))
# https://platformio.org/lib/show/1655/TinyGPSPlus # https://platformio.org/lib/show/1655/TinyGPSPlus
cg.add_library("mikalhart/TinyGPSPlus", "1.0.2") cg.add_library("mikalhart/TinyGPSPlus", "1.1.0")

View File

@ -10,6 +10,17 @@ static const char *const TAG = "gps";
TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); } TinyGPSPlus &GPSListener::get_tiny_gps() { return this->parent_->get_tiny_gps(); }
void GPS::dump_config() {
ESP_LOGCONFIG(TAG, "GPS:");
LOG_SENSOR(" ", "Latitude", this->latitude_sensor_);
LOG_SENSOR(" ", "Longitude", this->longitude_sensor_);
LOG_SENSOR(" ", "Speed", this->speed_sensor_);
LOG_SENSOR(" ", "Course", this->course_sensor_);
LOG_SENSOR(" ", "Altitude", this->altitude_sensor_);
LOG_SENSOR(" ", "Satellites", this->satellites_sensor_);
LOG_SENSOR(" ", "HDOP", this->hdop_sensor_);
}
void GPS::update() { void GPS::update() {
if (this->latitude_sensor_ != nullptr) if (this->latitude_sensor_ != nullptr)
this->latitude_sensor_->publish_state(this->latitude_); this->latitude_sensor_->publish_state(this->latitude_);
@ -34,40 +45,45 @@ void GPS::update() {
} }
void GPS::loop() { void GPS::loop() {
while (this->available() && !this->has_time_) { while (this->available() > 0 && !this->has_time_) {
if (this->tiny_gps_.encode(this->read())) { if (this->tiny_gps_.encode(this->read())) {
if (tiny_gps_.location.isUpdated()) { if (this->tiny_gps_.location.isUpdated()) {
this->latitude_ = tiny_gps_.location.lat(); this->latitude_ = this->tiny_gps_.location.lat();
this->longitude_ = tiny_gps_.location.lng(); this->longitude_ = this->tiny_gps_.location.lng();
ESP_LOGD(TAG, "Location:"); ESP_LOGD(TAG, "Location:");
ESP_LOGD(TAG, " Lat: %f", this->latitude_); ESP_LOGD(TAG, " Lat: %.6f °", this->latitude_);
ESP_LOGD(TAG, " Lon: %f", this->longitude_); ESP_LOGD(TAG, " Lon: %.6f °", this->longitude_);
} }
if (tiny_gps_.speed.isUpdated()) { if (this->tiny_gps_.speed.isUpdated()) {
this->speed_ = tiny_gps_.speed.kmph(); this->speed_ = this->tiny_gps_.speed.kmph();
ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_); ESP_LOGD(TAG, "Speed: %.3f km/h", this->speed_);
} }
if (tiny_gps_.course.isUpdated()) {
this->course_ = tiny_gps_.course.deg(); if (this->tiny_gps_.course.isUpdated()) {
this->course_ = this->tiny_gps_.course.deg();
ESP_LOGD(TAG, "Course: %.2f °", this->course_); ESP_LOGD(TAG, "Course: %.2f °", this->course_);
} }
if (tiny_gps_.altitude.isUpdated()) {
this->altitude_ = tiny_gps_.altitude.meters(); if (this->tiny_gps_.altitude.isUpdated()) {
this->altitude_ = this->tiny_gps_.altitude.meters();
ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_); ESP_LOGD(TAG, "Altitude: %.2f m", this->altitude_);
} }
if (tiny_gps_.satellites.isUpdated()) {
this->satellites_ = tiny_gps_.satellites.value(); if (this->tiny_gps_.satellites.isUpdated()) {
this->satellites_ = this->tiny_gps_.satellites.value();
ESP_LOGD(TAG, "Satellites: %d", this->satellites_); ESP_LOGD(TAG, "Satellites: %d", this->satellites_);
} }
if (tiny_gps_.hdop.isUpdated()) {
this->hdop_ = tiny_gps_.hdop.hdop(); if (this->tiny_gps_.hdop.isUpdated()) {
this->hdop_ = this->tiny_gps_.hdop.hdop();
ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_); ESP_LOGD(TAG, "HDOP: %.3f", this->hdop_);
} }
for (auto *listener : this->listeners_) for (auto *listener : this->listeners_) {
listener->on_update(this->tiny_gps_); listener->on_update(this->tiny_gps_);
}
} }
} }
} }

View File

@ -5,7 +5,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include "esphome/components/uart/uart.h" #include "esphome/components/uart/uart.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include <TinyGPS++.h> #include <TinyGPSPlus.h>
#include <vector> #include <vector>
@ -27,13 +27,13 @@ class GPSListener {
class GPS : public PollingComponent, public uart::UARTDevice { class GPS : public PollingComponent, public uart::UARTDevice {
public: public:
void set_latitude_sensor(sensor::Sensor *latitude_sensor) { latitude_sensor_ = latitude_sensor; } void set_latitude_sensor(sensor::Sensor *latitude_sensor) { this->latitude_sensor_ = latitude_sensor; }
void set_longitude_sensor(sensor::Sensor *longitude_sensor) { longitude_sensor_ = longitude_sensor; } void set_longitude_sensor(sensor::Sensor *longitude_sensor) { this->longitude_sensor_ = longitude_sensor; }
void set_speed_sensor(sensor::Sensor *speed_sensor) { speed_sensor_ = speed_sensor; } void set_speed_sensor(sensor::Sensor *speed_sensor) { this->speed_sensor_ = speed_sensor; }
void set_course_sensor(sensor::Sensor *course_sensor) { course_sensor_ = course_sensor; } void set_course_sensor(sensor::Sensor *course_sensor) { this->course_sensor_ = course_sensor; }
void set_altitude_sensor(sensor::Sensor *altitude_sensor) { altitude_sensor_ = altitude_sensor; } void set_altitude_sensor(sensor::Sensor *altitude_sensor) { this->altitude_sensor_ = altitude_sensor; }
void set_satellites_sensor(sensor::Sensor *satellites_sensor) { satellites_sensor_ = satellites_sensor; } void set_satellites_sensor(sensor::Sensor *satellites_sensor) { this->satellites_sensor_ = satellites_sensor; }
void set_hdop_sensor(sensor::Sensor *hdop_sensor) { hdop_sensor_ = hdop_sensor; } void set_hdop_sensor(sensor::Sensor *hdop_sensor) { this->hdop_sensor_ = hdop_sensor; }
void register_listener(GPSListener *listener) { void register_listener(GPSListener *listener) {
listener->parent_ = this; listener->parent_ = this;
@ -41,19 +41,20 @@ class GPS : public PollingComponent, public uart::UARTDevice {
} }
float get_setup_priority() const override { return setup_priority::HARDWARE; } float get_setup_priority() const override { return setup_priority::HARDWARE; }
void dump_config() override;
void loop() override; void loop() override;
void update() override; void update() override;
TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; } TinyGPSPlus &get_tiny_gps() { return this->tiny_gps_; }
protected: protected:
float latitude_ = NAN; float latitude_{NAN};
float longitude_ = NAN; float longitude_{NAN};
float speed_ = NAN; float speed_{NAN};
float course_ = NAN; float course_{NAN};
float altitude_ = NAN; float altitude_{NAN};
int satellites_ = 0; uint16_t satellites_{0};
double hdop_ = NAN; float hdop_{NAN};
sensor::Sensor *latitude_sensor_{nullptr}; sensor::Sensor *latitude_sensor_{nullptr};
sensor::Sensor *longitude_sensor_{nullptr}; sensor::Sensor *longitude_sensor_{nullptr};

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODEL from esphome.const import CONF_MODEL
CODEOWNERS = ["@orestismers"] CODEOWNERS = ["@orestismers"]
@ -21,16 +21,13 @@ MODELS = {
"yag": Model.GREE_YAG, "yag": Model.GREE_YAG,
} }
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(GreeClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(GreeClimate),
cv.Required(CONF_MODEL): cv.enum(MODELS), cv.Required(CONF_MODEL): cv.enum(MODELS),
} }
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL])) cg.add(var.set_model(config[CONF_MODEL]))
await climate_ir.register_climate_ir(var, config)

View File

@ -1,5 +1,6 @@
#include "growatt_solar.h" #include "growatt_solar.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace growatt_solar { namespace growatt_solar {
@ -18,7 +19,7 @@ void GrowattSolar::loop() {
void GrowattSolar::update() { void GrowattSolar::update() {
// If our last send has had no reply yet, and it wasn't that long ago, do nothing. // If our last send has had no reply yet, and it wasn't that long ago, do nothing.
uint32_t now = millis(); const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_send_ < this->get_update_interval() / 2) { if (now - this->last_send_ < this->get_update_interval() / 2) {
return; return;
} }

View File

@ -30,6 +30,7 @@ from esphome.const import (
CONF_VISUAL, CONF_VISUAL,
CONF_WIFI, CONF_WIFI,
) )
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv import esphome.final_validate as fv
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -185,42 +186,46 @@ def validate_visual(config):
return config return config
BASE_CONFIG_SCHEMA = ( def _base_config_schema(class_: MockObjClass) -> cv.Schema:
climate.CLIMATE_SCHEMA.extend( return (
{ climate.climate_schema(class_)
cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list( .extend(
cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True) {
), cv.Optional(CONF_SUPPORTED_MODES): cv.ensure_list(
cv.Optional( cv.enum(SUPPORTED_CLIMATE_MODES_OPTIONS, upper=True)
CONF_SUPPORTED_SWING_MODES, ),
default=[ cv.Optional(
"VERTICAL", CONF_SUPPORTED_SWING_MODES,
"HORIZONTAL", default=[
"BOTH", "VERTICAL",
], "HORIZONTAL",
): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)), "BOTH",
cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean, ],
cv.Optional(CONF_DISPLAY): cv.boolean, ): cv.ensure_list(cv.enum(SUPPORTED_SWING_MODES_OPTIONS, upper=True)),
cv.Optional( cv.Optional(CONF_WIFI_SIGNAL, default=False): cv.boolean,
CONF_ANSWER_TIMEOUT, cv.Optional(CONF_DISPLAY): cv.boolean,
): cv.positive_time_period_milliseconds, cv.Optional(
cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation( CONF_ANSWER_TIMEOUT,
{ ): cv.positive_time_period_milliseconds,
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StatusMessageTrigger), cv.Optional(CONF_ON_STATUS_MESSAGE): automation.validate_automation(
} {
), cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
} StatusMessageTrigger
),
}
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
) )
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
cv.typed_schema( cv.typed_schema(
{ {
PROTOCOL_SMARTAIR2: BASE_CONFIG_SCHEMA.extend( PROTOCOL_SMARTAIR2: _base_config_schema(Smartair2Climate).extend(
{ {
cv.GenerateID(): cv.declare_id(Smartair2Climate),
cv.Optional( cv.Optional(
CONF_ALTERNATIVE_SWING_CONTROL, default=False CONF_ALTERNATIVE_SWING_CONTROL, default=False
): cv.boolean, ): cv.boolean,
@ -232,9 +237,8 @@ CONFIG_SCHEMA = cv.All(
), ),
} }
), ),
PROTOCOL_HON: BASE_CONFIG_SCHEMA.extend( PROTOCOL_HON: _base_config_schema(HonClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(HonClimate),
cv.Optional( cv.Optional(
CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS" CONF_CONTROL_METHOD, default="SET_GROUP_PARAMETERS"
): cv.ensure_list( ): cv.ensure_list(
@ -464,10 +468,9 @@ FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config): async def to_code(config):
cg.add(haier_ns.init_haier_protocol_logging()) cg.add(haier_ns.init_haier_protocol_logging())
var = cg.new_Pvariable(config[CONF_ID]) var = await climate.new_climate(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await uart.register_uart_device(var, config) await uart.register_uart_device(var, config)
await climate.register_climate(var, config)
cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL])) cg.add(var.set_send_wifi(config[CONF_WIFI_SIGNAL]))
if CONF_CONTROL_METHOD in config: if CONF_CONTROL_METHOD in config:

View File

@ -2,7 +2,6 @@ import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_ID,
CONF_MAX_TEMPERATURE, CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE, CONF_MIN_TEMPERATURE,
CONF_PROTOCOL, CONF_PROTOCOL,
@ -98,9 +97,8 @@ VERTICAL_DIRECTIONS = {
} }
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( climate_ir.climate_ir_with_receiver_schema(HeatpumpIRClimate).extend(
{ {
cv.GenerateID(): cv.declare_id(HeatpumpIRClimate),
cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS), cv.Required(CONF_PROTOCOL): cv.enum(PROTOCOLS),
cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS), cv.Required(CONF_HORIZONTAL_DEFAULT): cv.enum(HORIZONTAL_DIRECTIONS),
cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS), cv.Required(CONF_VERTICAL_DEFAULT): cv.enum(VERTICAL_DIRECTIONS),
@ -112,8 +110,8 @@ CONFIG_SCHEMA = cv.All(
) )
def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await climate_ir.new_climate_ir(config)
if CONF_VISUAL not in config: if CONF_VISUAL not in config:
config[CONF_VISUAL] = {} config[CONF_VISUAL] = {}
visual = config[CONF_VISUAL] visual = config[CONF_VISUAL]
@ -121,7 +119,6 @@ def to_code(config):
visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE] visual[CONF_MAX_TEMPERATURE] = config[CONF_MAX_TEMPERATURE]
if CONF_MIN_TEMPERATURE not in visual: if CONF_MIN_TEMPERATURE not in visual:
visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE] visual[CONF_MIN_TEMPERATURE] = config[CONF_MIN_TEMPERATURE]
yield climate_ir.register_climate_ir(var, config)
cg.add(var.set_protocol(config[CONF_PROTOCOL])) cg.add(var.set_protocol(config[CONF_PROTOCOL]))
cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT])) cg.add(var.set_horizontal_default(config[CONF_HORIZONTAL_DEFAULT]))
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT])) cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))

View File

@ -1,20 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344") hitachi_ac344_ns = cg.esphome_ns.namespace("hitachi_ac344")
HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR) HitachiClimate = hitachi_ac344_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
{
cv.GenerateID(): cv.declare_id(HitachiClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -1,20 +1,13 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import climate_ir from esphome.components import climate_ir
import esphome.config_validation as cv
from esphome.const import CONF_ID
AUTO_LOAD = ["climate_ir"] AUTO_LOAD = ["climate_ir"]
hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424") hitachi_ac424_ns = cg.esphome_ns.namespace("hitachi_ac424")
HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR) HitachiClimate = hitachi_ac424_ns.class_("HitachiClimate", climate_ir.ClimateIR)
CONFIG_SCHEMA = climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA.extend( CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(HitachiClimate)
{
cv.GenerateID(): cv.declare_id(HitachiClimate),
}
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) await climate_ir.new_climate_ir(config)
await climate_ir.register_climate_ir(var, config)

View File

@ -25,13 +25,13 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(HTE501Component), cv.GenerateID(): cv.declare_id(HTE501Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema( cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY,
@ -49,10 +49,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature_sensor(sens)) cg.add(var.set_temperature_sensor(sens))
if CONF_HUMIDITY in config: if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config[CONF_HUMIDITY]) sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity_sensor(sens)) cg.add(var.set_humidity_sensor(sens))

View File

@ -23,13 +23,13 @@ CONFIG_SCHEMA = (
cv.Schema( cv.Schema(
{ {
cv.GenerateID(): cv.declare_id(HYT271Component), cv.GenerateID(): cv.declare_id(HYT271Component),
cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
unit_of_measurement=UNIT_CELSIUS, unit_of_measurement=UNIT_CELSIUS,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_TEMPERATURE, device_class=DEVICE_CLASS_TEMPERATURE,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Required(CONF_HUMIDITY): sensor.sensor_schema( cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,
accuracy_decimals=1, accuracy_decimals=1,
device_class=DEVICE_CLASS_HUMIDITY, device_class=DEVICE_CLASS_HUMIDITY,
@ -47,10 +47,10 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await i2c.register_i2c_device(var, config) await i2c.register_i2c_device(var, config)
if CONF_TEMPERATURE in config: if temperature := config.get(CONF_TEMPERATURE):
sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) sens = await sensor.new_sensor(temperature)
cg.add(var.set_temperature(sens)) cg.add(var.set_temperature(sens))
if CONF_HUMIDITY in config: if humidity := config.get(CONF_HUMIDITY):
sens = await sensor.new_sensor(config[CONF_HUMIDITY]) sens = await sensor.new_sensor(humidity)
cg.add(var.set_humidity(sens)) cg.add(var.set_humidity(sens))

View File

@ -4,6 +4,7 @@ from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import ( from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -74,6 +75,7 @@ I2S_PORTS = {
VARIANT_ESP32S2: 1, VARIANT_ESP32S2: 1,
VARIANT_ESP32S3: 2, VARIANT_ESP32S3: 2,
VARIANT_ESP32C3: 1, VARIANT_ESP32C3: 1,
VARIANT_ESP32P4: 3,
} }
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t") i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")

View File

@ -1,5 +1,6 @@
#include "kuntze.h" #include "kuntze.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome { namespace esphome {
namespace kuntze { namespace kuntze {
@ -60,7 +61,7 @@ void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
} }
void Kuntze::loop() { void Kuntze::loop() {
uint32_t now = millis(); uint32_t now = App.get_loop_component_start_time();
// timeout after 15 seconds // timeout after 15 seconds
if (this->waiting_ && (now - this->last_send_ > 15000)) { if (this->waiting_ && (now - this->last_send_ > 15000)) {
ESP_LOGW(TAG, "timed out waiting for response"); ESP_LOGW(TAG, "timed out waiting for response");

View File

@ -1,5 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Callable
import esphome.codegen as cg import esphome.codegen as cg

View File

@ -8,8 +8,10 @@ from esphome.components.esp32.const import (
VARIANT_ESP32, VARIANT_ESP32,
VARIANT_ESP32C2, VARIANT_ESP32C2,
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@ -24,6 +26,7 @@ from esphome.const import (
CONF_HARDWARE_UART, CONF_HARDWARE_UART,
CONF_ID, CONF_ID,
CONF_LEVEL, CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS, CONF_LOGS,
CONF_ON_MESSAGE, CONF_ON_MESSAGE,
CONF_TAG, CONF_TAG,
@ -87,8 +90,10 @@ UART_SELECTION_ESP32 = {
VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C2: [UART0, UART1], VARIANT_ESP32C2: [UART0, UART1],
VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG], VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
} }
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1] UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
@ -204,8 +209,10 @@ CONFIG_SCHEMA = cv.All(
esp32_s3_idf=USB_SERIAL_JTAG, esp32_s3_idf=USB_SERIAL_JTAG,
esp32_c3_arduino=USB_CDC, esp32_c3_arduino=USB_CDC,
esp32_c3_idf=USB_SERIAL_JTAG, esp32_c3_idf=USB_SERIAL_JTAG,
esp32_c5_idf=USB_SERIAL_JTAG,
esp32_c6_arduino=USB_CDC, esp32_c6_arduino=USB_CDC,
esp32_c6_idf=USB_SERIAL_JTAG, esp32_c6_idf=USB_SERIAL_JTAG,
esp32_p4_idf=USB_SERIAL_JTAG,
rp2040=USB_CDC, rp2040=USB_CDC,
bk72xx=DEFAULT, bk72xx=DEFAULT,
rtl87xx=DEFAULT, rtl87xx=DEFAULT,
@ -247,6 +254,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
baud_rate = config[CONF_BAUD_RATE] baud_rate = config[CONF_BAUD_RATE]
level = config[CONF_LEVEL] level = config[CONF_LEVEL]
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
log = cg.new_Pvariable( log = cg.new_Pvariable(
config[CONF_ID], config[CONF_ID],
@ -254,6 +262,7 @@ async def to_code(config):
config[CONF_TX_BUFFER_SIZE], config[CONF_TX_BUFFER_SIZE],
) )
if CORE.is_esp32: if CORE.is_esp32:
cg.add(log.create_pthread_key())
task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE] task_log_buffer_size = config[CONF_TASK_LOG_BUFFER_SIZE]
if task_log_buffer_size > 0: if task_log_buffer_size > 0:
cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER") cg.add_define("USE_ESPHOME_TASK_LOG_BUFFER")

View File

@ -14,25 +14,47 @@ namespace logger {
static const char *const TAG = "logger"; static const char *const TAG = "logger";
#ifdef USE_ESP32 #ifdef USE_ESP32
// Implementation for ESP32 (multi-core with atomic support) // Implementation for ESP32 (multi-task platform with task-specific tracking)
// Main thread: synchronous logging with direct buffer access // Main task always uses direct buffer access for console output and callbacks
// Other threads: console output with stack buffer, callbacks via async buffer //
// For non-main tasks:
// - WITH task log buffer: Prefer sending to ring buffer for async processing
// - Avoids allocating stack memory for console output in normal operation
// - Prevents console corruption from concurrent writes by multiple tasks
// - Messages are serialized through main loop for proper console output
// - Fallback to emergency console logging only if ring buffer is full
// - WITHOUT task log buffer: Only emergency console output, no callbacks
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_.load(std::memory_order_relaxed)) if (level > this->level_for(tag))
return; return;
recursion_guard_.store(true, std::memory_order_relaxed);
TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
bool is_main_task = (current_task == main_task_);
// For main task: call log_message_to_buffer_and_send_ which does console and callback logging // Check and set recursion guard - uses pthread TLS for per-task state
if (current_task == main_task_) { if (this->check_and_set_task_log_recursion_(is_main_task)) {
return; // Recursion detected
}
// Main task uses the shared buffer for efficiency
if (is_main_task) {
this->log_message_to_buffer_and_send_(level, tag, line, format, args); this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_.store(false, std::memory_order_release); this->reset_task_log_recursion_(is_main_task);
return; return;
} }
// For non-main tasks: use stack-allocated buffer only for console output bool message_sent = false;
if (this->baud_rate_ > 0) { // If logging is enabled, write to console #ifdef USE_ESPHOME_TASK_LOG_BUFFER
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered
message_sent = this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag,
static_cast<uint16_t>(line), current_task, format, args);
#endif // USE_ESPHOME_TASK_LOG_BUFFER
// Emergency console logging for non-main tasks when ring buffer is full or disabled
// This is a fallback mechanism to ensure critical log messages are visible
// Note: This may cause interleaved/corrupted console output if multiple tasks
// log simultaneously, but it's better than losing important messages entirely
if (!message_sent && this->baud_rate_ > 0) { // If logging is enabled, write to console
// Maximum size for console log messages (includes null terminator) // Maximum size for console log messages (includes null terminator)
static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144; static const size_t MAX_CONSOLE_LOG_MSG_SIZE = 144;
char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety
@ -42,32 +64,21 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
this->write_msg_(console_buffer); this->write_msg_(console_buffer);
} }
#ifdef USE_ESPHOME_TASK_LOG_BUFFER // Reset the recursion guard for this task
// For non-main tasks, queue the message for callbacks - but only if we have any callbacks registered this->reset_task_log_recursion_(is_main_task);
if (this->log_callback_.size() > 0) {
// This will be processed in the main loop
this->log_buffer_->send_message_thread_safe(static_cast<uint8_t>(level), tag, static_cast<uint16_t>(line),
current_task, format, args);
}
#endif // USE_ESPHOME_TASK_LOG_BUFFER
recursion_guard_.store(false, std::memory_order_release);
} }
#endif // USE_ESP32 #else
// Implementation for all other platforms
#ifndef USE_ESP32
// Implementation for platforms that do not support atomic operations
// or have to consider logging in other tasks
void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *format, va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
recursion_guard_ = true; global_recursion_guard_ = true;
// Format and send to both console and callbacks // Format and send to both console and callbacks
this->log_message_to_buffer_and_send_(level, tag, line, format, args); this->log_message_to_buffer_and_send_(level, tag, line, format, args);
recursion_guard_ = false; global_recursion_guard_ = false;
} }
#endif // !USE_ESP32 #endif // !USE_ESP32
@ -76,10 +87,10 @@ void HOT Logger::log_vprintf_(int level, const char *tag, int line, const char *
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266. // Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format, void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStringHelper *format,
va_list args) { // NOLINT va_list args) { // NOLINT
if (level > this->level_for(tag) || recursion_guard_) if (level > this->level_for(tag) || global_recursion_guard_)
return; return;
recursion_guard_ = true; global_recursion_guard_ = true;
this->tx_buffer_at_ = 0; this->tx_buffer_at_ = 0;
// Copy format string from progmem // Copy format string from progmem
@ -91,7 +102,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
// Buffer full from copying format // Buffer full from copying format
if (this->tx_buffer_at_ >= this->tx_buffer_size_) { if (this->tx_buffer_at_ >= this->tx_buffer_size_) {
recursion_guard_ = false; // Make sure to reset the recursion guard before returning global_recursion_guard_ = false; // Make sure to reset the recursion guard before returning
return; return;
} }
@ -107,7 +118,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
} }
this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start);
recursion_guard_ = false; global_recursion_guard_ = false;
} }
#endif // USE_STORE_LOG_STR_IN_FLASH #endif // USE_STORE_LOG_STR_IN_FLASH
@ -179,7 +190,17 @@ void Logger::loop() {
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_);
// At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token); this->log_buffer_->release_message_main_loop(received_token);
// Write to console from the main loop to prevent corruption from concurrent writes
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
} }
} }
#endif #endif

View File

@ -3,7 +3,7 @@
#include <cstdarg> #include <cstdarg>
#include <map> #include <map>
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <atomic> #include <pthread.h>
#endif #endif
#include "esphome/core/automation.h" #include "esphome/core/automation.h"
#include "esphome/core/component.h" #include "esphome/core/component.h"
@ -84,6 +84,23 @@ enum UARTSelection {
}; };
#endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY #endif // USE_ESP32 || USE_ESP8266 || USE_RP2040 || USE_LIBRETINY
/**
* @brief Logger component for all ESPHome logging.
*
* This class implements a multi-platform logging system with protection against recursion.
*
* Recursion Protection Strategy:
* - On ESP32: Uses task-specific recursion guards
* * Main task: Uses a dedicated boolean member variable for efficiency
* * Other tasks: Uses pthread TLS with a dynamically allocated key for task-specific state
* - On other platforms: Uses a simple global recursion guard
*
* We use pthread TLS via pthread_key_create to create a unique key for storing
* task-specific recursion state, which:
* 1. Efficiently handles multiple tasks without locks or mutexes
* 2. Works with ESP-IDF's pthread implementation that uses a linked list for TLS variables
* 3. Avoids the limitations of the fixed FreeRTOS task local storage slots
*/
class Logger : public Component { class Logger : public Component {
public: public:
explicit Logger(uint32_t baud_rate, size_t tx_buffer_size); explicit Logger(uint32_t baud_rate, size_t tx_buffer_size);
@ -102,6 +119,9 @@ class Logger : public Component {
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
uart_port_t get_uart_num() const { return uart_num_; } uart_port_t get_uart_num() const { return uart_num_; }
#endif #endif
#ifdef USE_ESP32
void create_pthread_key() { pthread_key_create(&log_recursion_key_, nullptr); }
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY)
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; } void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
/// Get the UART used by the logger. /// Get the UART used by the logger.
@ -222,18 +242,22 @@ class Logger : public Component {
std::map<std::string, int> log_levels_{}; std::map<std::string, int> log_levels_{};
CallbackManager<void(int, const char *, const char *)> log_callback_{}; CallbackManager<void(int, const char *, const char *)> log_callback_{};
int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE}; int current_level_{ESPHOME_LOG_LEVEL_VERY_VERBOSE};
#ifdef USE_ESP32
std::atomic<bool> recursion_guard_{false};
#ifdef USE_ESPHOME_TASK_LOG_BUFFER #ifdef USE_ESPHOME_TASK_LOG_BUFFER
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
#endif #endif
#ifdef USE_ESP32
// Task-specific recursion guards:
// - Main task uses a dedicated member variable for efficiency
// - Other tasks use pthread TLS with a dynamically created key via pthread_key_create
bool main_task_recursion_guard_{false};
pthread_key_t log_recursion_key_;
#else #else
bool recursion_guard_{false}; bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif #endif
void *main_task_ = nullptr;
CallbackManager<void(int)> level_callback_{}; CallbackManager<void(int)> level_callback_{};
#if defined(USE_ESP32) || defined(USE_LIBRETINY) #if defined(USE_ESP32) || defined(USE_LIBRETINY)
void *main_task_ = nullptr; // Only used for thread name identification
const char *HOT get_thread_name_() { const char *HOT get_thread_name_() {
TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); TaskHandle_t current_task = xTaskGetCurrentTaskHandle();
if (current_task == main_task_) { if (current_task == main_task_) {
@ -248,6 +272,32 @@ class Logger : public Component {
} }
#endif #endif
#ifdef USE_ESP32
inline bool HOT check_and_set_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
const bool was_recursive = main_task_recursion_guard_;
main_task_recursion_guard_ = true;
return was_recursive;
}
intptr_t current = (intptr_t) pthread_getspecific(log_recursion_key_);
if (current != 0)
return true;
pthread_setspecific(log_recursion_key_, (void *) 1);
return false;
}
inline void HOT reset_task_log_recursion_(bool is_main_task) {
if (is_main_task) {
main_task_recursion_guard_ = false;
return;
}
pthread_setspecific(log_recursion_key_, (void *) 0);
}
#endif
inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer, inline void HOT write_header_to_buffer_(int level, const char *tag, int line, const char *thread_name, char *buffer,
int *buffer_at, int buffer_size) { int *buffer_at, int buffer_size) {
// Format header // Format header

View File

@ -18,12 +18,12 @@
#endif #endif
#endif #endif
#include "freertos/FreeRTOS.h"
#include "esp_idf_version.h" #include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <fcntl.h>
#endif // USE_ESP_IDF #endif // USE_ESP_IDF
@ -174,11 +174,11 @@ void Logger::pre_setup() {
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
void HOT Logger::write_msg_(const char *msg) { void HOT Logger::write_msg_(const char *msg) {
if ( if (
#if defined(USE_ESP32_VARIANT_ESP32S2) #if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC this->uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) #elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_ESP32_VARIANT_ESP32S3) #elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#else #else
/* DISABLES CODE */ (false) // NOLINT /* DISABLES CODE */ (false) // NOLINT

View File

@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_
from esphome.core import CORE from esphome.core import CORE
from esphome.cpp_helpers import register_component, register_parented from esphome.cpp_helpers import register_component, register_parented
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns
CODEOWNERS = ["@clydebarrow"] CODEOWNERS = ["@clydebarrow"]
@ -21,9 +21,10 @@ CONFIG_SCHEMA = select.select_schema(
async def to_code(config): async def to_code(config):
levels = LOG_LEVEL_SEVERITY parent = await cg.get_variable(config[CONF_LOGGER_ID])
index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL]) levels = list(LOG_LEVELS)
index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL])
levels = levels[: index + 1] levels = levels[: index + 1]
var = await select.new_select(config, options=levels) var = await select.new_select(config, options=levels)
await register_parented(var, config[CONF_LOGGER_ID]) await register_parented(var, parent)
await register_component(var, config) await register_component(var, config)

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