1
0
mirror of https://github.com/esphome/esphome.git synced 2025-09-17 02:32:20 +01:00

Merge branch 'dev' into multi_device

This commit is contained in:
DanielV
2025-05-22 08:41:54 +02:00
committed by GitHub
422 changed files with 17009 additions and 3702 deletions

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

@@ -57,6 +57,17 @@ jobs:
event: 'REQUEST_CHANGES', event: 'REQUEST_CHANGES',
body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.' body: 'You have altered the generated proto files but they do not match what is expected.\nPlease run "script/api_protobuf/api_protobuf.py" and commit the changes.'
}) })
- if: failure()
name: Show changes
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@v4.6.2
with:
name: generated-proto-files
path: |
esphome/components/api/api_pb2.*
esphome/components/api/api_pb2_service.*
- if: success() - if: success()
name: Dismiss review name: Dismiss review
uses: actions/github-script@v7.0.1 uses: actions/github-script@v7.0.1

View File

@@ -47,7 +47,7 @@ jobs:
- 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
@@ -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,18 +185,18 @@ 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
@@ -221,7 +221,7 @@ jobs:
. venv/bin/activate . venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests pytest -vv --cov-report=xml --tb=native 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 }}
@@ -292,6 +292,11 @@ jobs:
name: Run script/clang-tidy for ESP32 IDF name: Run script/clang-tidy for ESP32 IDF
options: --environment esp32-idf-tidy --grep USE_ESP_IDF options: --environment esp32-idf-tidy --grep USE_ESP_IDF
pio_cache_key: tidyesp32-idf pio_cache_key: tidyesp32-idf
- id: clang-tidy
name: Run script/clang-tidy for ZEPHYR
options: --environment nrf52-tidy --grep USE_ZEPHYR
pio_cache_key: tidy-zephyr
ignore_errors: true
steps: steps:
- name: Check out code from GitHub - name: Check out code from GitHub
@@ -331,13 +336,13 @@ jobs:
- name: Run clang-tidy - name: Run clang-tidy
run: | run: |
. venv/bin/activate . venv/bin/activate
script/clang-tidy --all-headers --fix ${{ matrix.options }} script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
env: env:
# Also cache libdeps, store them in a ~/.platformio subfolder # Also cache libdeps, store them in a ~/.platformio subfolder
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
- name: Suggested changes - name: Suggested changes
run: script/ci-suggest-changes run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
# yamllint disable-line rule:line-length # yamllint disable-line rule:line-length
if: always() if: always()

View File

@@ -18,6 +18,7 @@ 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.1.7
- name: Get tag - name: Get tag
@@ -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:
@@ -56,16 +65,14 @@ jobs:
uses: actions/setup-python@v5.6.0 uses: actions/setup-python@v5.6.0
with: with:
python-version: "3.x" python-version: "3.x"
- name: Set up python environment
env:
ESPHOME_NO_VENV: 1
run: script/setup
- name: Build - name: Build
run: |- run: |-
pip3 install build pip3 install build
python3 -m build python3 -m build
- name: Publish - name: Publish
uses: pypa/gh-action-pypi-publish@v1.12.4 uses: pypa/gh-action-pypi-publish@v1.12.4
with:
skip-existing: true
deploy-docker: deploy-docker:
name: Build ESPHome ${{ matrix.platform.arch }} name: Build ESPHome ${{ matrix.platform.arch }}
@@ -89,7 +96,7 @@ jobs:
- 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
@@ -231,3 +238,24 @@ jobs:
content: description content: description
} }
}) })
deploy-esphome-schema:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Trigger Workflow
uses: actions/github-script@v7.0.1
with:
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "esphome-schema",
workflow_id: "generate-schemas.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

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

1
.gitignore vendored
View File

@@ -143,3 +143,4 @@ sdkconfig.*
/components /components
/managed_components /managed_components
api-docs/

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.0 rev: v0.11.10
hooks: hooks:
# Run the linter. # Run the linter.
- id: ruff - id: ruff
@@ -28,12 +28,12 @@ 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.19.1
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.35.1 rev: v1.37.1
hooks: hooks:
- id: yamllint - id: yamllint
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format

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
@@ -282,6 +283,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov esphome/components/midea_ir/* @dudanov
esphome/components/mipi_spi/* @clydebarrow
esphome/components/mitsubishi/* @RubyBailey esphome/components/mitsubishi/* @RubyBailey
esphome/components/mixer/speaker/* @kahrendt esphome/components/mixer/speaker/* @kahrendt
esphome/components/mlx90393/* @functionpointer esphome/components/mlx90393/* @functionpointer
@@ -398,6 +400,7 @@ esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz esphome/components/sn74hc165/* @jesserockz
esphome/components/socket/* @esphome/core esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
esphome/components/speaker/* @jesserockz @kahrendt esphome/components/speaker/* @jesserockz @kahrendt
esphome/components/speaker/media_player/* @kahrendt @synesthesiam esphome/components/speaker/media_player/* @kahrendt @synesthesiam
esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi/* @clydebarrow @esphome/core
@@ -476,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

@@ -34,7 +34,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_(
BASE_SCHEMA = ( BASE_SCHEMA = (
sensor.SENSOR_SCHEMA.extend( cv.Schema(
{ {
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT, unit_of_measurement=UNIT_PERCENT,

View File

@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CODE, CONF_CODE,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_STATE, CONF_ON_STATE,
@@ -12,6 +14,7 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
CODEOWNERS = ["@grahambrown11", "@hwstar"] CODEOWNERS = ["@grahambrown11", "@hwstar"]
@@ -78,12 +81,11 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
"AlarmControlPanelCondition", automation.Condition "AlarmControlPanelCondition", automation.Condition
) )
ALARM_CONTROL_PANEL_SCHEMA = ( _ALARM_CONTROL_PANEL_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(AlarmControlPanel),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTAlarmControlPanelComponent mqtt.MQTTAlarmControlPanelComponent
), ),
@@ -146,6 +148,33 @@ ALARM_CONTROL_PANEL_SCHEMA = (
) )
) )
def alarm_control_panel_schema(
class_: MockObjClass,
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
# Remove before 2025.11.0
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
cv.deprecated_schema_constant("alarm_control_panel")
)
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.GenerateID(): cv.use_id(AlarmControlPanel), cv.GenerateID(): cv.use_id(AlarmControlPanel),
@@ -209,6 +238,12 @@ async def register_alarm_control_panel(var, config):
await setup_alarm_control_panel_core_(var, config) await setup_alarm_control_panel_core_(var, config)
async def new_alarm_control_panel(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_alarm_control_panel(var, config)
return var
@automation.register_action( @automation.register_action(
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
) )

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import ble_client, cover from esphome.components import ble_client, cover
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PIN from esphome.const import CONF_PIN
CODEOWNERS = ["@buxtronix"] CODEOWNERS = ["@buxtronix"]
DEPENDENCIES = ["ble_client"] DEPENDENCIES = ["ble_client"]
@@ -15,9 +15,9 @@ Am43Component = am43_ns.class_(
) )
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
cover.COVER_SCHEMA.extend( cover.cover_schema(Am43Component)
.extend(
{ {
cv.GenerateID(): cv.declare_id(Am43Component),
cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
} }
@@ -28,9 +28,8 @@ CONFIG_SCHEMA = (
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
cg.add(var.set_pin(config[CONF_PIN])) cg.add(var.set_pin(config[CONF_PIN]))
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
await cg.register_component(var, config) await cg.register_component(var, config)
await cover.register_cover(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)

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

@@ -33,23 +33,24 @@ service APIConnection {
rpc execute_service (ExecuteServiceRequest) returns (void) {} rpc execute_service (ExecuteServiceRequest) returns (void) {}
rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
rpc cover_command (CoverCommandRequest) returns (void) {} rpc button_command (ButtonCommandRequest) returns (void) {}
rpc fan_command (FanCommandRequest) returns (void) {}
rpc light_command (LightCommandRequest) returns (void) {}
rpc switch_command (SwitchCommandRequest) returns (void) {}
rpc camera_image (CameraImageRequest) returns (void) {} rpc camera_image (CameraImageRequest) returns (void) {}
rpc climate_command (ClimateCommandRequest) returns (void) {} rpc climate_command (ClimateCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {} rpc cover_command (CoverCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
rpc button_command (ButtonCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc valve_command (ValveCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
rpc date_command (DateCommandRequest) returns (void) {} rpc date_command (DateCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc datetime_command (DateTimeCommandRequest) returns (void) {} rpc datetime_command (DateTimeCommandRequest) returns (void) {}
rpc fan_command (FanCommandRequest) returns (void) {}
rpc light_command (LightCommandRequest) returns (void) {}
rpc lock_command (LockCommandRequest) returns (void) {}
rpc media_player_command (MediaPlayerCommandRequest) returns (void) {}
rpc number_command (NumberCommandRequest) returns (void) {}
rpc select_command (SelectCommandRequest) returns (void) {}
rpc siren_command (SirenCommandRequest) returns (void) {}
rpc switch_command (SwitchCommandRequest) returns (void) {}
rpc text_command (TextCommandRequest) returns (void) {}
rpc time_command (TimeCommandRequest) returns (void) {}
rpc update_command (UpdateCommandRequest) returns (void) {} rpc update_command (UpdateCommandRequest) returns (void) {}
rpc valve_command (ValveCommandRequest) returns (void) {}
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
@@ -442,7 +443,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;
@@ -670,7 +672,7 @@ message SubscribeLogsResponse {
option (no_delay) = false; option (no_delay) = false;
LogLevel level = 1; LogLevel level = 1;
string message = 3; bytes message = 3;
bool send_failed = 4; bool send_failed = 4;
} }
@@ -928,6 +930,7 @@ message ClimateStateResponse {
float target_temperature = 4; float target_temperature = 4;
float target_temperature_low = 5; float target_temperature_low = 5;
float target_temperature_high = 6; float target_temperature_high = 6;
// For older peers, equal to preset == CLIMATE_PRESET_AWAY
bool unused_legacy_away = 7; bool unused_legacy_away = 7;
ClimateAction action = 8; ClimateAction action = 8;
ClimateFanMode fan_mode = 9; ClimateFanMode fan_mode = 9;
@@ -953,6 +956,7 @@ message ClimateCommandRequest {
float target_temperature_low = 7; float target_temperature_low = 7;
bool has_target_temperature_high = 8; bool has_target_temperature_high = 8;
float target_temperature_high = 9; float target_temperature_high = 9;
// legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset
bool unused_has_legacy_away = 10; bool unused_has_legacy_away = 10;
bool unused_legacy_away = 11; bool unused_legacy_away = 11;
bool has_fan_mode = 12; bool has_fan_mode = 12;
@@ -1057,6 +1061,49 @@ message SelectCommandRequest {
string state = 2; string state = 2;
} }
// ==================== SIREN ====================
message ListEntitiesSirenResponse {
option (id) = 55;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string unique_id = 4;
string icon = 5;
bool disabled_by_default = 6;
repeated string tones = 7;
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
}
message SirenStateResponse {
option (id) = 56;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
fixed32 key = 1;
bool state = 2;
}
message SirenCommandRequest {
option (id) = 57;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
fixed32 key = 1;
bool has_state = 2;
bool state = 3;
bool has_tone = 4;
string tone = 5;
bool has_duration = 6;
uint32 duration = 7;
bool has_volume = 8;
float volume = 9;
}
// ==================== LOCK ==================== // ==================== LOCK ====================
enum LockState { enum LockState {
@@ -1230,8 +1277,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
message BluetoothServiceData { message BluetoothServiceData {
string uuid = 1; string uuid = 1;
repeated uint32 legacy_data = 2 [deprecated = true]; repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7
bytes data = 3; // Changed in proto version 1.7 bytes data = 3; // Added in api version 1.7
} }
message BluetoothLEAdvertisementResponse { message BluetoothLEAdvertisementResponse {
option (id) = 67; option (id) = 67;
@@ -1240,7 +1287,7 @@ message BluetoothLEAdvertisementResponse {
option (no_delay) = true; option (no_delay) = true;
uint64 address = 1; uint64 address = 1;
string name = 2; bytes name = 2;
sint32 rssi = 3; sint32 rssi = 3;
repeated string service_uuids = 4; repeated string service_uuids = 4;
@@ -1527,7 +1574,7 @@ message BluetoothScannerSetModeRequest {
BluetoothScannerMode mode = 1; BluetoothScannerMode mode = 1;
} }
// ==================== PUSH TO TALK ==================== // ==================== VOICE ASSISTANT ====================
enum VoiceAssistantSubscribeFlag { enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;

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;
@@ -312,11 +405,20 @@ class APIConnection : public APIServerConnection {
void on_fatal_error() override; void on_fatal_error() override;
void on_unauthenticated_access() override; void on_unauthenticated_access() override;
void on_no_setup_connection() override; void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer() 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();
// 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_; }
@@ -324,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

@@ -5,6 +5,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "proto.h" #include "proto.h"
#include "api_pb2_size.h"
#include <cstring> #include <cstring>
namespace esphome { namespace esphome {
@@ -72,6 +73,91 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN"; return "UNKNOWN";
} }
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError APIFrameHelper::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) {
// This method writes data to socket or buffers it
// 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
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf.empty()) {
// try to empty tx_buf first
while (!tx_buf.empty()) {
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
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
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
}
}
if (!tx_buf.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
// 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 (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
// 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
} else if (sent == -1) {
// an error occurred
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
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t remaining = total_write_len - sent;
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + remaining);
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK; // Success, data buffered
}
return APIError::OK; // Success, all data sent
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets // uncomment to log raw packets
//#define HELPER_LOG_PACKETS //#define HELPER_LOG_PACKETS
@@ -407,9 +493,12 @@ 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;
@@ -471,7 +560,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK; return APIError::OK;
} }
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
int err; int err;
APIError aerr; APIError aerr;
aerr = state_action_(); aerr = state_action_();
@@ -483,31 +572,36 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
return APIError::WOULD_BLOCK; return APIError::WOULD_BLOCK;
} }
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding
size_t payload_len = raw_buffer->size() - frame_header_padding_;
size_t padding = 0; size_t padding = 0;
size_t msg_len = 4 + payload_len + padding; size_t msg_len = 4 + payload_len + padding;
size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_);
auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]};
if (tmpbuf == nullptr) {
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;
@@ -516,11 +610,13 @@ APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload
} }
size_t total_len = 3 + mbuf.size; size_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
@@ -546,71 +642,6 @@ APIError APINoiseFrameHelper::try_send_tx_buf_() {
return APIError::OK; return APIError::OK;
} }
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
if (iovcnt == 0)
return APIError::OK;
APIError aerr;
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
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;
}
ssize_t sent = socket_->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
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;
} else if (sent == -1) {
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { 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
@@ -697,6 +728,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;
@@ -744,6 +777,11 @@ 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
@@ -804,6 +842,10 @@ 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;
// 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 = socket_->read(&data, 1); ssize_t received = socket_->read(&data, 1);
if (received == -1) { if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) { if (errno == EWOULDBLOCK || errno == EAGAIN) {
@@ -817,27 +859,60 @@ 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 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;
rx_header_parsed_len_ = msg_size_varint->as_uint32(); rx_header_parsed_len_ = msg_size_varint->as_uint32();
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;
@@ -883,7 +958,7 @@ 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;
} }
@@ -927,26 +1002,66 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK; return APIError::OK;
} }
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
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.push_back(0x00); // Message data starts after padding (frame_header_padding_ = 6)
ProtoVarInt(payload_len).encode(header); size_t payload_len = raw_buffer->size() - frame_header_padding_;
ProtoVarInt(type).encode(header);
struct iovec iov[2]; // Calculate varint sizes for header components
iov[0].iov_base = &header[0]; size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
iov[0].iov_len = header.size(); size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
if (payload_len == 0) { size_t total_header_len = 1 + size_varint_len + type_varint_len;
return write_raw_(iov, 1);
if (total_header_len > frame_header_padding_) {
// Header is too large to fit in the padding
return APIError::BAD_ARG;
} }
iov[1].iov_base = const_cast<uint8_t *>(payload);
iov[1].iov_len = payload_len;
return write_raw_(iov, 2); // 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();
size_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::try_send_tx_buf_() { APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf // try send from tx_buf
@@ -966,71 +1081,6 @@ APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
return APIError::OK; return APIError::OK;
} }
/** Write the data to the socket, or buffer it a write would block
*
* @param data The data to write
* @param len The length of data
*/
APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
if (iovcnt == 0)
return APIError::OK;
APIError aerr;
size_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
}
if (!tx_buf_.empty()) {
// try to empty tx_buf_ first
aerr = try_send_tx_buf_();
if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK)
return aerr;
}
if (!tx_buf_.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
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;
}
ssize_t sent = socket_->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
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;
} else if (sent == -1) {
// an error occurred
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
} else {
tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
to_consume = 0;
}
}
return APIError::OK;
}
// fully sent
return APIError::OK;
}
APIError APIPlaintextFrameHelper::close() { APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED; state_ = State::CLOSED;
@@ -1048,6 +1098,11 @@ APIError APIPlaintextFrameHelper::shutdown(int how) {
} }
return APIError::OK; 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

@@ -16,6 +16,8 @@
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;
@@ -65,26 +67,46 @@ class APIFrameHelper {
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; virtual bool can_write_without_blocking() = 0;
virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0; virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
virtual std::string getpeername() = 0; virtual std::string getpeername() = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0; virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual APIError close() = 0; virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0; virtual APIError shutdown(int how) = 0;
// Give this helper a name for logging // Give this helper a name for logging
virtual void set_log_info(std::string info) = 0; virtual void set_log_info(std::string info) = 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:
// Common implementation for writing raw data to socket
template<typename StateEnum>
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);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
}; };
#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))) {} : socket_(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; bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); } std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen); return this->socket_->getpeername(addr, addrlen);
@@ -93,6 +115,10 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override; APIError shutdown(int how) override;
// Give this helper a name for logging // Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); } void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
struct ParsedFrame { struct ParsedFrame {
@@ -103,7 +129,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len); APIError write_frame_(const uint8_t *data, size_t len);
APIError write_raw_(const struct iovec *iov, int iovcnt); 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);
@@ -111,6 +139,9 @@ class APINoiseFrameHelper : public APIFrameHelper {
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::string info_; std::string info_;
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3]; uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0; size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_; std::vector<uint8_t> rx_buf_;
@@ -141,13 +172,20 @@ class APINoiseFrameHelper : public APIFrameHelper {
#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) : socket_(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; bool can_write_without_blocking() override;
APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override; APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); } std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override { int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen); return this->socket_->getpeername(addr, addrlen);
@@ -156,6 +194,10 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError shutdown(int how) override; APIError shutdown(int how) override;
// Give this helper a name for logging // Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); } void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected: protected:
struct ParsedFrame { struct ParsedFrame {
@@ -164,12 +206,23 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError try_read_frame_(ParsedFrame *frame); APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_(); APIError try_send_tx_buf_();
APIError write_raw_(const struct iovec *iov, int iovcnt); inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
std::unique_ptr<socket::Socket> socket_; std::unique_ptr<socket::Socket> socket_;
std::string info_; std::string info_;
std::vector<uint8_t> rx_header_buf_; // Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need:
// 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// 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
// ESP32 due to memory constraints.
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; uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0; uint32_t rx_header_parsed_len_ = 0;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// This file was automatically generated with a tool. // This file was automatically generated with a tool.
// See scripts/api_protobuf/api_protobuf.py // See script/api_protobuf/api_protobuf.py
#include "api_pb2_service.h" #include "api_pb2_service.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
@@ -292,6 +292,24 @@ bool APIServerConnectionBase::send_select_state_response(const SelectStateRespon
#endif #endif
#ifdef USE_SELECT #ifdef USE_SELECT
#endif #endif
#ifdef USE_SIREN
bool APIServerConnectionBase::send_list_entities_siren_response(const ListEntitiesSirenResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_list_entities_siren_response: %s", msg.dump().c_str());
#endif
return this->send_message_<ListEntitiesSirenResponse>(msg, 55);
}
#endif
#ifdef USE_SIREN
bool APIServerConnectionBase::send_siren_state_response(const SirenStateResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "send_siren_state_response: %s", msg.dump().c_str());
#endif
return this->send_message_<SirenStateResponse>(msg, 56);
}
#endif
#ifdef USE_SIREN
#endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) { bool APIServerConnectionBase::send_list_entities_lock_response(const ListEntitiesLockResponse &msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@@ -903,6 +921,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str()); ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
#endif #endif
this->on_select_command_request(msg); this->on_select_command_request(msg);
#endif
break;
}
case 57: {
#ifdef USE_SIREN
SirenCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
#endif
this->on_siren_command_request(msg);
#endif #endif
break; break;
} }
@@ -1369,8 +1398,8 @@ void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncrypt
} }
} }
#endif #endif
#ifdef USE_COVER #ifdef USE_BUTTON
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
this->on_no_setup_connection(); this->on_no_setup_connection();
return; return;
@@ -1379,46 +1408,7 @@ void APIServerConnection::on_cover_command_request(const CoverCommandRequest &ms
this->on_unauthenticated_access(); this->on_unauthenticated_access();
return; return;
} }
this->cover_command(msg); this->button_command(msg);
}
#endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->fan_command(msg);
}
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->light_command(msg);
}
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->switch_command(msg);
} }
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
@@ -1447,8 +1437,8 @@ void APIServerConnection::on_climate_command_request(const ClimateCommandRequest
this->climate_command(msg); this->climate_command(msg);
} }
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
this->on_no_setup_connection(); this->on_no_setup_connection();
return; return;
@@ -1457,85 +1447,7 @@ void APIServerConnection::on_number_command_request(const NumberCommandRequest &
this->on_unauthenticated_access(); this->on_unauthenticated_access();
return; return;
} }
this->number_command(msg); this->cover_command(msg);
}
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->text_command(msg);
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->select_command(msg);
}
#endif
#ifdef USE_BUTTON
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->button_command(msg);
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->lock_command(msg);
}
#endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->valve_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
@@ -1551,19 +1463,6 @@ void APIServerConnection::on_date_command_request(const DateCommandRequest &msg)
this->date_command(msg); this->date_command(msg);
} }
#endif #endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->time_command(msg);
}
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
@@ -1577,6 +1476,136 @@ void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequ
this->datetime_command(msg); this->datetime_command(msg);
} }
#endif #endif
#ifdef USE_FAN
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->fan_command(msg);
}
#endif
#ifdef USE_LIGHT
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->light_command(msg);
}
#endif
#ifdef USE_LOCK
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->lock_command(msg);
}
#endif
#ifdef USE_MEDIA_PLAYER
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->media_player_command(msg);
}
#endif
#ifdef USE_NUMBER
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->number_command(msg);
}
#endif
#ifdef USE_SELECT
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->select_command(msg);
}
#endif
#ifdef USE_SIREN
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->siren_command(msg);
}
#endif
#ifdef USE_SWITCH
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->switch_command(msg);
}
#endif
#ifdef USE_TEXT
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->text_command(msg);
}
#endif
#ifdef USE_DATETIME_TIME
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->time_command(msg);
}
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) {
if (!this->is_connection_setup()) { if (!this->is_connection_setup()) {
@@ -1590,6 +1619,19 @@ void APIServerConnection::on_update_command_request(const UpdateCommandRequest &
this->update_command(msg); this->update_command(msg);
} }
#endif #endif
#ifdef USE_VALVE
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) {
if (!this->is_connection_setup()) {
this->on_no_setup_connection();
return;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return;
}
this->valve_command(msg);
}
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
const SubscribeBluetoothLEAdvertisementsRequest &msg) { const SubscribeBluetoothLEAdvertisementsRequest &msg) {

View File

@@ -1,5 +1,5 @@
// This file was automatically generated with a tool. // This file was automatically generated with a tool.
// See scripts/api_protobuf/api_protobuf.py // See script/api_protobuf/api_protobuf.py
#pragma once #pragma once
#include "api_pb2.h" #include "api_pb2.h"
@@ -136,6 +136,15 @@ class APIServerConnectionBase : public ProtoService {
#ifdef USE_SELECT #ifdef USE_SELECT
virtual void on_select_command_request(const SelectCommandRequest &value){}; virtual void on_select_command_request(const SelectCommandRequest &value){};
#endif #endif
#ifdef USE_SIREN
bool send_list_entities_siren_response(const ListEntitiesSirenResponse &msg);
#endif
#ifdef USE_SIREN
bool send_siren_state_response(const SirenStateResponse &msg);
#endif
#ifdef USE_SIREN
virtual void on_siren_command_request(const SirenCommandRequest &value){};
#endif
#ifdef USE_LOCK #ifdef USE_LOCK
bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg); bool send_list_entities_lock_response(const ListEntitiesLockResponse &msg);
#endif #endif
@@ -364,17 +373,8 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0; virtual NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) = 0;
#endif #endif
#ifdef USE_COVER #ifdef USE_BUTTON
virtual void cover_command(const CoverCommandRequest &msg) = 0; virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_FAN
virtual void fan_command(const FanCommandRequest &msg) = 0;
#endif
#ifdef USE_LIGHT
virtual void light_command(const LightCommandRequest &msg) = 0;
#endif
#ifdef USE_SWITCH
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
virtual void camera_image(const CameraImageRequest &msg) = 0; virtual void camera_image(const CameraImageRequest &msg) = 0;
@@ -382,39 +382,51 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
virtual void climate_command(const ClimateCommandRequest &msg) = 0; virtual void climate_command(const ClimateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
virtual void number_command(const NumberCommandRequest &msg) = 0; virtual void cover_command(const CoverCommandRequest &msg) = 0;
#endif
#ifdef USE_TEXT
virtual void text_command(const TextCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_BUTTON
virtual void button_command(const ButtonCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
virtual void date_command(const DateCommandRequest &msg) = 0; virtual void date_command(const DateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0; virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_FAN
virtual void fan_command(const FanCommandRequest &msg) = 0;
#endif
#ifdef USE_LIGHT
virtual void light_command(const LightCommandRequest &msg) = 0;
#endif
#ifdef USE_LOCK
virtual void lock_command(const LockCommandRequest &msg) = 0;
#endif
#ifdef USE_MEDIA_PLAYER
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
#endif
#ifdef USE_NUMBER
virtual void number_command(const NumberCommandRequest &msg) = 0;
#endif
#ifdef USE_SELECT
virtual void select_command(const SelectCommandRequest &msg) = 0;
#endif
#ifdef USE_SIREN
virtual void siren_command(const SirenCommandRequest &msg) = 0;
#endif
#ifdef USE_SWITCH
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
#endif
#ifdef USE_TEXT
virtual void text_command(const TextCommandRequest &msg) = 0;
#endif
#ifdef USE_DATETIME_TIME
virtual void time_command(const TimeCommandRequest &msg) = 0;
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
virtual void update_command(const UpdateCommandRequest &msg) = 0; virtual void update_command(const UpdateCommandRequest &msg) = 0;
#endif #endif
#ifdef USE_VALVE
virtual void valve_command(const ValveCommandRequest &msg) = 0;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
#endif #endif
@@ -478,17 +490,8 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_API_NOISE #ifdef USE_API_NOISE
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override; void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
#endif #endif
#ifdef USE_COVER #ifdef USE_BUTTON
void on_cover_command_request(const CoverCommandRequest &msg) override; void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_FAN
void on_fan_command_request(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &msg) override;
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif #endif
#ifdef USE_ESP32_CAMERA #ifdef USE_ESP32_CAMERA
void on_camera_image_request(const CameraImageRequest &msg) override; void on_camera_image_request(const CameraImageRequest &msg) override;
@@ -496,39 +499,51 @@ class APIServerConnection : public APIServerConnectionBase {
#ifdef USE_CLIMATE #ifdef USE_CLIMATE
void on_climate_command_request(const ClimateCommandRequest &msg) override; void on_climate_command_request(const ClimateCommandRequest &msg) override;
#endif #endif
#ifdef USE_NUMBER #ifdef USE_COVER
void on_number_command_request(const NumberCommandRequest &msg) override; void on_cover_command_request(const CoverCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_BUTTON
void on_button_command_request(const ButtonCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_DATE #ifdef USE_DATETIME_DATE
void on_date_command_request(const DateCommandRequest &msg) override; void on_date_command_request(const DateCommandRequest &msg) override;
#endif #endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_DATETIME #ifdef USE_DATETIME_DATETIME
void on_date_time_command_request(const DateTimeCommandRequest &msg) override; void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
#endif #endif
#ifdef USE_FAN
void on_fan_command_request(const FanCommandRequest &msg) override;
#endif
#ifdef USE_LIGHT
void on_light_command_request(const LightCommandRequest &msg) override;
#endif
#ifdef USE_LOCK
void on_lock_command_request(const LockCommandRequest &msg) override;
#endif
#ifdef USE_MEDIA_PLAYER
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
#endif
#ifdef USE_NUMBER
void on_number_command_request(const NumberCommandRequest &msg) override;
#endif
#ifdef USE_SELECT
void on_select_command_request(const SelectCommandRequest &msg) override;
#endif
#ifdef USE_SIREN
void on_siren_command_request(const SirenCommandRequest &msg) override;
#endif
#ifdef USE_SWITCH
void on_switch_command_request(const SwitchCommandRequest &msg) override;
#endif
#ifdef USE_TEXT
void on_text_command_request(const TextCommandRequest &msg) override;
#endif
#ifdef USE_DATETIME_TIME
void on_time_command_request(const TimeCommandRequest &msg) override;
#endif
#ifdef USE_UPDATE #ifdef USE_UPDATE
void on_update_command_request(const UpdateCommandRequest &msg) override; void on_update_command_request(const UpdateCommandRequest &msg) override;
#endif #endif
#ifdef USE_VALVE
void on_valve_command_request(const ValveCommandRequest &msg) override;
#endif
#ifdef USE_BLUETOOTH_PROXY #ifdef USE_BLUETOOTH_PROXY
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
#endif #endif

View File

@@ -0,0 +1,361 @@
#pragma once
#include "proto.h"
#include <cstdint>
#include <string>
namespace esphome {
namespace api {
class ProtoSize {
public:
/**
* @brief ProtoSize class for Protocol Buffer serialization size calculation
*
* This class provides static methods to calculate the exact byte counts needed
* for encoding various Protocol Buffer field types. All methods are designed to be
* efficient for the common case where many fields have default values.
*
* Implements Protocol Buffer encoding size calculation according to:
* https://protobuf.dev/programming-guides/encoding/
*
* Key features:
* - Early-return optimization for zero/default values
* - Direct total_size updates to avoid unnecessary additions
* - Specialized handling for different field types according to protobuf spec
* - Templated helpers for repeated fields and messages
*/
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
* @param value The uint32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint32_t value) {
// Optimized varint size calculation using leading zeros
// Each 7 bits requires one byte in the varint encoding
if (value < 128)
return 1; // 7 bits, common case for small values
// For larger values, count bytes needed based on the position of the highest bit set
if (value < 16384) {
return 2; // 14 bits
} else if (value < 2097152) {
return 3; // 21 bits
} else if (value < 268435456) {
return 4; // 28 bits
} else {
return 5; // 32 bits (maximum for uint32_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
*
* @param value The uint64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(uint64_t value) {
// Handle common case of values fitting in uint32_t (vast majority of use cases)
if (value <= UINT32_MAX) {
return varint(static_cast<uint32_t>(value));
}
// For larger values, determine size based on highest bit position
if (value < (1ULL << 35)) {
return 5; // 35 bits
} else if (value < (1ULL << 42)) {
return 6; // 42 bits
} else if (value < (1ULL << 49)) {
return 7; // 49 bits
} else if (value < (1ULL << 56)) {
return 8; // 56 bits
} else if (value < (1ULL << 63)) {
return 9; // 63 bits
} else {
return 10; // 64 bits (maximum for uint64_t)
}
}
/**
* @brief Calculates the size in bytes needed to encode an int32_t value as a varint
*
* Special handling is needed for negative values, which are sign-extended to 64 bits
* in Protocol Buffers, resulting in a 10-byte varint.
*
* @param value The int32_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int32_t value) {
// Negative values are sign-extended to 64 bits in protocol buffers,
// which always results in a 10-byte varint for negative int32
if (value < 0) {
return 10; // Negative int32 is always 10 bytes long
}
// For non-negative values, use the uint32_t implementation
return varint(static_cast<uint32_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode an int64_t value as a varint
*
* @param value The int64_t value to calculate size for
* @return The number of bytes needed to encode the value
*/
static inline uint32_t varint(int64_t value) {
// For int64_t, we convert to uint64_t and calculate the size
// This works because the bit pattern determines the encoding size,
// and we've handled negative int32 values as a special case above
return varint(static_cast<uint64_t>(value));
}
/**
* @brief Calculates the size in bytes needed to encode a field ID and wire type
*
* @param field_id The field identifier
* @param type The wire type value (from the WireType enum in the protobuf spec)
* @return The number of bytes needed to encode the field ID and wire type
*/
static inline uint32_t field(uint32_t field_id, uint32_t type) {
uint32_t tag = (field_id << 3) | (type & 0b111);
return varint(tag);
}
/**
* @brief Common parameters for all add_*_field methods
*
* All add_*_field methods follow these common patterns:
*
* @param total_size Reference to the total message size to update
* @param field_id_size Pre-calculated size of the field ID in bytes
* @param value The value to calculate size for (type varies)
* @param force Whether to calculate size even if the value is default/zero/empty
*
* Each method follows this implementation pattern:
* 1. Skip calculation if value is default (0, false, empty) and not forced
* 2. Calculate the size based on the field's encoding rules
* 3. Add the field_id_size + calculated value size to total_size
*/
/**
* @brief Calculates and adds the size of an int32 field to the total message size
*/
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
if (value < 0) {
// Negative values are encoded as 10-byte varints in protobuf
total_size += field_id_size + 10;
} else {
// For non-negative values, use the standard varint size
total_size += field_id_size + varint(static_cast<uint32_t>(value));
}
}
/**
* @brief Calculates and adds the size of a uint32 field to the total message size
*/
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a boolean field to the total message size
*/
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value, bool force = false) {
// Skip calculation if value is false and not forced
if (!value && !force) {
return; // No need to update total_size
}
// Boolean fields always use 1 byte when true
total_size += field_id_size + 1;
}
/**
* @brief Calculates and adds the size of a fixed field to the total message size
*
* Fixed fields always take exactly N bytes (4 for fixed32/float, 8 for fixed64/double).
*
* @tparam NumBytes The number of bytes for this fixed field (4 or 8)
* @param is_nonzero Whether the value is non-zero
*/
template<uint32_t NumBytes>
static inline void add_fixed_field(uint32_t &total_size, uint32_t field_id_size, bool is_nonzero,
bool force = false) {
// Skip calculation if value is zero and not forced
if (!is_nonzero && !force) {
return; // No need to update total_size
}
// Fixed fields always take exactly NumBytes
total_size += field_id_size + NumBytes;
}
/**
* @brief Calculates and adds the size of an enum field to the total message size
*
* Enum fields are encoded as uint32 varints.
*/
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Enums are encoded as uint32
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint32 field to the total message size
*
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of an int64 field to the total message size
*/
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a uint64 field to the total message size
*/
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value,
bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
total_size += field_id_size + varint(value);
}
/**
* @brief Calculates and adds the size of a sint64 field to the total message size
*
* Sint64 fields use ZigZag encoding, which is more efficient for negative values.
*/
static inline void add_sint64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value, bool force = false) {
// Skip calculation if value is zero and not forced
if (value == 0 && !force) {
return; // No need to update total_size
}
// ZigZag encoding for sint64: (n << 1) ^ (n >> 63)
uint64_t zigzag = (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
total_size += field_id_size + varint(zigzag);
}
/**
* @brief Calculates and adds the size of a string/bytes field to the total message size
*/
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, const std::string &str,
bool force = false) {
// Skip calculation if string is empty and not forced
if (str.empty() && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
const uint32_t str_size = static_cast<uint32_t>(str.size());
total_size += field_id_size + varint(str_size) + str_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This helper function directly updates the total_size reference if the nested size
* is greater than zero or force is true.
*
* @param nested_size The pre-calculated size of the nested message
*/
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size,
bool force = false) {
// Skip calculation if nested message is empty and not forced
if (nested_size == 0 && !force) {
return; // No need to update total_size
}
// Calculate and directly add to total_size
// Field ID + length varint + nested message content
total_size += field_id_size + varint(nested_size) + nested_size;
}
/**
* @brief Calculates and adds the size of a nested message field to the total message size
*
* This templated version directly takes a message object, calculates its size internally,
* and updates the total_size reference. This eliminates the need for a temporary variable
* at the call site.
*
* @tparam MessageType The type of the nested message (inferred from parameter)
* @param message The nested message object
*/
template<typename MessageType>
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const MessageType &message,
bool force = false) {
uint32_t nested_size = 0;
message.calculate_size(nested_size);
// Use the base implementation with the calculated nested_size
add_message_field(total_size, field_id_size, nested_size, force);
}
/**
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
*
* This helper processes a vector of message objects, calculating the size for each message
* and adding it to the total size.
*
* @tparam MessageType The type of the nested messages in the vector
* @param messages Vector of message objects
*/
template<typename MessageType>
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
const std::vector<MessageType> &messages) {
// Skip if the vector is empty
if (messages.empty()) {
return;
}
// For repeated fields, always use force=true
for (const auto &message : messages) {
add_message_object(total_size, field_id_size, message, true);
}
}
};
} // namespace api
} // namespace esphome

View File

@@ -126,19 +126,29 @@ void APIServer::loop() {
conn->start(); conn->start();
} }
// Partition clients into remove and active // Process clients and remove disconnected ones in a single pass
auto new_end = std::partition(this->clients_.begin(), this->clients_.end(), if (!this->clients_.empty()) {
[](const std::unique_ptr<APIConnection> &conn) { return !conn->remove_; }); size_t client_index = 0;
// print disconnection messages while (client_index < this->clients_.size()) {
for (auto it = new_end; it != this->clients_.end(); ++it) { auto &client = this->clients_[client_index];
this->client_disconnected_trigger_->trigger((*it)->client_info_, (*it)->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", (*it)->client_info_.c_str());
}
// resize vector
this->clients_.erase(new_end, this->clients_.end());
for (auto &client : this->clients_) { if (client->remove_) {
client->loop(); // Handle disconnection
this->client_disconnected_trigger_->trigger(client->client_info_, client->client_peername_);
ESP_LOGV(TAG, "Removing connection to %s", client->client_info_.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
}
this->clients_.pop_back();
// Don't increment client_index since we need to process the swapped element
} else {
// Process active client
client->loop();
client_index++; // Move to next client
}
}
} }
if (this->reboot_timeout_ != 0) { if (this->reboot_timeout_ != 0) {

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,7 +50,9 @@ class ProtoVarInt {
} }
} }
return {}; if (consumed != nullptr)
*consumed = 0;
return {}; // Incomplete or invalid varint
} }
uint32_t as_uint32() const { return this->value_; } uint32_t as_uint32() const { return this->value_; }
@@ -71,6 +83,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) {
@@ -149,6 +189,18 @@ class ProtoWriteBuffer {
void write(uint8_t value) { this->buffer_->push_back(value); } void write(uint8_t value) { this->buffer_->push_back(value); }
void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
/**
* Encode a field key (tag/wire type combination).
*
* @param field_id Field number (tag) in the protobuf message
* @param type Wire type value:
* - 0: Varint (int32, int64, uint32, uint64, sint32, sint64, bool, enum)
* - 1: 64-bit (fixed64, sfixed64, double)
* - 2: Length-delimited (string, bytes, embedded messages, packed repeated fields)
* - 5: 32-bit (fixed32, sfixed32, float)
*
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { void encode_field_raw(uint32_t field_id, uint32_t type) {
uint32_t val = (field_id << 3) | (type & 0b111); uint32_t val = (field_id << 3) | (type & 0b111);
this->encode_varint_raw(val); this->encode_varint_raw(val);
@@ -157,7 +209,7 @@ class ProtoWriteBuffer {
if (len == 0 && !force) if (len == 0 && !force)
return; return;
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len); this->encode_varint_raw(len);
auto *data = reinterpret_cast<const uint8_t *>(string); auto *data = reinterpret_cast<const uint8_t *>(string);
this->buffer_->insert(this->buffer_->end(), data, data + len); this->buffer_->insert(this->buffer_->end(), data, data + len);
@@ -171,26 +223,26 @@ class ProtoWriteBuffer {
void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) { void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
this->encode_varint_raw(value); this->encode_varint_raw(value);
} }
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw(ProtoVarInt(value)); this->encode_varint_raw(ProtoVarInt(value));
} }
void encode_bool(uint32_t field_id, bool value, bool force = false) { void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force) if (!value && !force)
return; return;
this->encode_field_raw(field_id, 0); this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->write(0x01); this->write(0x01);
} }
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) { void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 5); this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->write((value >> 0) & 0xFF); this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF); this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
@@ -200,7 +252,7 @@ class ProtoWriteBuffer {
if (value == 0 && !force) if (value == 0 && !force)
return; return;
this->encode_field_raw(field_id, 5); this->encode_field_raw(field_id, 1); // type 1: 64-bit fixed64
this->write((value >> 0) & 0xFF); this->write((value >> 0) & 0xFF);
this->write((value >> 8) & 0xFF); this->write((value >> 8) & 0xFF);
this->write((value >> 16) & 0xFF); this->write((value >> 16) & 0xFF);
@@ -254,7 +306,7 @@ class ProtoWriteBuffer {
this->encode_uint64(field_id, uvalue, force); this->encode_uint64(field_id, uvalue, force);
} }
template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) { template<class C> void encode_message(uint32_t field_id, const C &value, bool force = false) {
this->encode_field_raw(field_id, 2); this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
size_t begin = this->buffer_->size(); size_t begin = this->buffer_->size();
value.encode(*this); value.encode(*this);
@@ -276,6 +328,7 @@ class ProtoMessage {
virtual ~ProtoMessage() = default; virtual ~ProtoMessage() = default;
virtual void encode(ProtoWriteBuffer buffer) const = 0; virtual void encode(ProtoWriteBuffer buffer) const = 0;
void decode(const uint8_t *buffer, size_t length); void decode(const uint8_t *buffer, size_t length);
virtual void calculate_size(uint32_t &total_size) const = 0;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
std::string dump() const; std::string dump() const;
virtual void dump_to(std::string &out) const = 0; virtual void dump_to(std::string &out) const = 0;
@@ -298,13 +351,29 @@ class ProtoService {
virtual void on_fatal_error() = 0; virtual void on_fatal_error() = 0;
virtual void on_unauthenticated_access() = 0; virtual void on_unauthenticated_access() = 0;
virtual void on_no_setup_connection() = 0; virtual void on_no_setup_connection() = 0;
virtual ProtoWriteBuffer create_buffer() = 0; /**
* Create a buffer with a reserved size.
* @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint
* to optimize memory usage and avoid reallocations during encoding.
* Implementations should aim to allocate at least this size.
* @return A ProtoWriteBuffer object with the reserved size.
*/
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0; virtual bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) = 0;
virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0; virtual bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
// Optimized method that pre-allocates buffer based on message size
template<class C> bool send_message_(const C &msg, uint32_t message_type) { template<class C> bool send_message_(const C &msg, uint32_t message_type) {
auto buffer = this->create_buffer(); uint32_t msg_size = 0;
msg.calculate_size(msg_size);
// Create a pre-sized buffer
auto buffer = this->create_buffer(msg_size);
// Encode message into the buffer
msg.encode(buffer); msg.encode(buffer);
// Send the buffer
return this->send_buffer(buffer, message_type); return this->send_buffer(buffer, message_type);
} }
}; };

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

@@ -3,5 +3,6 @@ import esphome.codegen as cg
CODEOWNERS = ["@circuitsetup", "@descipher"] CODEOWNERS = ["@circuitsetup", "@descipher"]
atm90e32_ns = cg.esphome_ns.namespace("atm90e32") atm90e32_ns = cg.esphome_ns.namespace("atm90e32")
ATM90E32Component = atm90e32_ns.class_("ATM90E32Component", cg.Component)
CONF_ATM90E32_ID = "atm90e32_id" CONF_ATM90E32_ID = "atm90e32_id"

View File

@@ -1,7 +1,7 @@
#include "atm90e32.h" #include "atm90e32.h"
#include "atm90e32_reg.h"
#include "esphome/core/log.h"
#include <cinttypes> #include <cinttypes>
#include <cmath>
#include "esphome/core/log.h"
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {
@@ -11,115 +11,84 @@ void ATM90E32Component::loop() {
if (this->get_publish_interval_flag_()) { if (this->get_publish_interval_flag_()) {
this->set_publish_interval_flag_(false); this->set_publish_interval_flag_(false);
for (uint8_t phase = 0; phase < 3; phase++) { for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) { if (this->phase_[phase].voltage_sensor_ != nullptr)
this->phase_[phase].voltage_ = this->get_phase_voltage_(phase); this->phase_[phase].voltage_ = this->get_phase_voltage_(phase);
}
} if (this->phase_[phase].current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_ = this->get_phase_current_(phase); this->phase_[phase].current_ = this->get_phase_current_(phase);
}
} if (this->phase_[phase].power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].active_power_ = this->get_phase_active_power_(phase); this->phase_[phase].active_power_ = this->get_phase_active_power_(phase);
}
} if (this->phase_[phase].power_factor_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase); this->phase_[phase].power_factor_ = this->get_phase_power_factor_(phase);
}
} if (this->phase_[phase].reactive_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase); this->phase_[phase].reactive_power_ = this->get_phase_reactive_power_(phase);
}
} if (this->phase_[phase].apparent_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) { this->phase_[phase].apparent_power_ = this->get_phase_apparent_power_(phase);
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr)
this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase); this->phase_[phase].forward_active_energy_ = this->get_phase_forward_active_energy_(phase);
}
} if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase); this->phase_[phase].reverse_active_energy_ = this->get_phase_reverse_active_energy_(phase);
}
} if (this->phase_[phase].phase_angle_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase); this->phase_[phase].phase_angle_ = this->get_phase_angle_(phase);
}
} if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase); this->phase_[phase].harmonic_active_power_ = this->get_phase_harmonic_active_power_(phase);
}
} if (this->phase_[phase].peak_current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase); this->phase_[phase].peak_current_ = this->get_phase_peak_current_(phase);
}
} // After the local store is collected we can publish them trusting they are within +-1 hardware sampling
// After the local store in collected we can publish them trusting they are withing +-1 haardware sampling if (this->phase_[phase].voltage_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].voltage_sensor_ != nullptr) {
this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase)); this->phase_[phase].voltage_sensor_->publish_state(this->get_local_phase_voltage_(phase));
}
} if (this->phase_[phase].current_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].current_sensor_ != nullptr) {
this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase)); this->phase_[phase].current_sensor_->publish_state(this->get_local_phase_current_(phase));
}
} if (this->phase_[phase].power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_sensor_ != nullptr) {
this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase)); this->phase_[phase].power_sensor_->publish_state(this->get_local_phase_active_power_(phase));
}
} if (this->phase_[phase].power_factor_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].power_factor_sensor_ != nullptr) {
this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase)); this->phase_[phase].power_factor_sensor_->publish_state(this->get_local_phase_power_factor_(phase));
}
} if (this->phase_[phase].reactive_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reactive_power_sensor_ != nullptr) {
this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase)); this->phase_[phase].reactive_power_sensor_->publish_state(this->get_local_phase_reactive_power_(phase));
}
} if (this->phase_[phase].apparent_power_sensor_ != nullptr)
for (uint8_t phase = 0; phase < 3; phase++) { this->phase_[phase].apparent_power_sensor_->publish_state(this->get_local_phase_apparent_power_(phase));
if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) { if (this->phase_[phase].forward_active_energy_sensor_ != nullptr) {
this->phase_[phase].forward_active_energy_sensor_->publish_state( this->phase_[phase].forward_active_energy_sensor_->publish_state(
this->get_local_phase_forward_active_energy_(phase)); this->get_local_phase_forward_active_energy_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) { if (this->phase_[phase].reverse_active_energy_sensor_ != nullptr) {
this->phase_[phase].reverse_active_energy_sensor_->publish_state( this->phase_[phase].reverse_active_energy_sensor_->publish_state(
this->get_local_phase_reverse_active_energy_(phase)); this->get_local_phase_reverse_active_energy_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) { if (this->phase_[phase].phase_angle_sensor_ != nullptr)
if (this->phase_[phase].phase_angle_sensor_ != nullptr) {
this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase)); this->phase_[phase].phase_angle_sensor_->publish_state(this->get_local_phase_angle_(phase));
}
}
for (uint8_t phase = 0; phase < 3; phase++) {
if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) { if (this->phase_[phase].harmonic_active_power_sensor_ != nullptr) {
this->phase_[phase].harmonic_active_power_sensor_->publish_state( this->phase_[phase].harmonic_active_power_sensor_->publish_state(
this->get_local_phase_harmonic_active_power_(phase)); this->get_local_phase_harmonic_active_power_(phase));
} }
}
for (uint8_t phase = 0; phase < 3; phase++) { if (this->phase_[phase].peak_current_sensor_ != nullptr)
if (this->phase_[phase].peak_current_sensor_ != nullptr) {
this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase)); this->phase_[phase].peak_current_sensor_->publish_state(this->get_local_phase_peak_current_(phase));
}
} }
if (this->freq_sensor_ != nullptr) { if (this->freq_sensor_ != nullptr)
this->freq_sensor_->publish_state(this->get_frequency_()); this->freq_sensor_->publish_state(this->get_frequency_());
}
if (this->chip_temperature_sensor_ != nullptr) { if (this->chip_temperature_sensor_ != nullptr)
this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_()); this->chip_temperature_sensor_->publish_state(this->get_chip_temperature_());
}
} }
} }
@@ -130,82 +99,30 @@ void ATM90E32Component::update() {
} }
this->set_publish_interval_flag_(true); this->set_publish_interval_flag_(true);
this->status_clear_warning(); this->status_clear_warning();
}
void ATM90E32Component::restore_calibrations_() { #ifdef USE_TEXT_SENSOR
if (enable_offset_calibration_) { this->check_phase_status();
this->pref_.load(&this->offset_phase_); this->check_over_current();
} this->check_freq_status();
}; #endif
void ATM90E32Component::run_offset_calibrations() {
// Run the calibrations and
// Setup voltage and current calibration offsets for PHASE A
this->offset_phase_[PHASEA].voltage_offset_ = calibrate_voltage_offset_phase(PHASEA);
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = calibrate_current_offset_phase(PHASEA);
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE B
this->offset_phase_[PHASEB].voltage_offset_ = calibrate_voltage_offset_phase(PHASEB);
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = calibrate_current_offset_phase(PHASEB);
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
// Setup voltage and current calibration offsets for PHASE C
this->offset_phase_[PHASEC].voltage_offset_ = calibrate_voltage_offset_phase(PHASEC);
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = calibrate_current_offset_phase(PHASEC);
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
}
void ATM90E32Component::clear_offset_calibrations() {
// Clear the calibrations and
this->offset_phase_[PHASEA].voltage_offset_ = 0;
this->phase_[PHASEA].voltage_offset_ = this->offset_phase_[PHASEA].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETA, this->phase_[PHASEA].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEA].current_offset_ = 0;
this->phase_[PHASEA].current_offset_ = this->offset_phase_[PHASEA].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETA, this->phase_[PHASEA].current_offset_); // C Current offset
this->offset_phase_[PHASEB].voltage_offset_ = 0;
this->phase_[PHASEB].voltage_offset_ = this->offset_phase_[PHASEB].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETB, this->phase_[PHASEB].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEB].current_offset_ = 0;
this->phase_[PHASEB].current_offset_ = this->offset_phase_[PHASEB].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETB, this->phase_[PHASEB].current_offset_); // C Current offset
this->offset_phase_[PHASEC].voltage_offset_ = 0;
this->phase_[PHASEC].voltage_offset_ = this->offset_phase_[PHASEC].voltage_offset_;
this->write16_(ATM90E32_REGISTER_UOFFSETC, this->phase_[PHASEC].voltage_offset_); // C Voltage offset
this->offset_phase_[PHASEC].current_offset_ = 0;
this->phase_[PHASEC].current_offset_ = this->offset_phase_[PHASEC].current_offset_;
this->write16_(ATM90E32_REGISTER_IOFFSETC, this->phase_[PHASEC].current_offset_); // C Current offset
this->pref_.save(&this->offset_phase_);
ESP_LOGI(TAG, "PhaseA Vo=%5d PhaseB Vo=%5d PhaseC Vo=%5d", this->offset_phase_[PHASEA].voltage_offset_,
this->offset_phase_[PHASEB].voltage_offset_, this->offset_phase_[PHASEC].voltage_offset_);
ESP_LOGI(TAG, "PhaseA Io=%5d PhaseB Io=%5d PhaseC Io=%5d", this->offset_phase_[PHASEA].current_offset_,
this->offset_phase_[PHASEB].current_offset_, this->offset_phase_[PHASEC].current_offset_);
} }
void ATM90E32Component::setup() { void ATM90E32Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component..."); ESP_LOGCONFIG(TAG, "Setting up ATM90E32 Component...");
this->spi_setup(); this->spi_setup();
if (this->enable_offset_calibration_) {
uint32_t hash = fnv1_hash(App.get_friendly_name());
this->pref_ = global_preferences->make_preference<Calibration[3]>(hash, true);
this->restore_calibrations_();
}
uint16_t mmode0 = 0x87; // 3P4W 50Hz uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0;
uint16_t low_thresh = 0;
if (line_freq_ == 60) { if (line_freq_ == 60) {
mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz
// for freq threshold registers
high_thresh = 6300; // 63.00 Hz
low_thresh = 5700; // 57.00 Hz
} else {
high_thresh = 5300; // 53.00 Hz
low_thresh = 4700; // 47.00 Hz
} }
if (current_phases_ == 2) { if (current_phases_ == 2) {
@@ -216,34 +133,84 @@ void ATM90E32Component::setup() {
this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A); // Perform soft reset
delay(6); // Wait for the minimum 5ms + 1ms delay(6); // Wait for the minimum 5ms + 1ms
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA); // enable register config access
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != 0x55AA) { if (!this->validate_spi_read_(0x55AA, "setup()")) {
ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings"); ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
this->mark_failed(); this->mark_failed();
return; return;
} }
this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering this->write16_(ATM90E32_REGISTER_METEREN, 0x0001); // Enable Metering
this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time ms (15:8), Sag Period ms (7:0) this->write16_(ATM90E32_REGISTER_SAGPEAKDETCFG, 0xFF3F); // Peak Detector time (15:8) 255ms, Sag Period (7:0) 63ms
this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000 this->write16_(ATM90E32_REGISTER_PLCONSTH, 0x0861); // PL Constant MSB (default) = 140625000
this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default) this->write16_(ATM90E32_REGISTER_PLCONSTL, 0xC468); // PL Constant LSB (default)
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // ZX2, ZX1, ZX0 pin config this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program) this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold
this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500 this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50% this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750 this->write16_(ATM90E32_REGISTER_PPHASETH, 0x02EE); // Each Phase Active Phase Threshold - 0.002A/0.00032 = 750
this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10% this->write16_(ATM90E32_REGISTER_QPHASETH, 0x02EE); // Each phase Reactive Phase Threshold - 10%
// Setup voltage and current gain for PHASE A
this->write16_(ATM90E32_REGISTER_UGAINA, this->phase_[PHASEA].voltage_gain_); // A Voltage rms gain if (this->enable_offset_calibration_) {
this->write16_(ATM90E32_REGISTER_IGAINA, this->phase_[PHASEA].ct_gain_); // A line current gain // Initialize flash storage for offset calibrations
// Setup voltage and current gain for PHASE B uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
this->write16_(ATM90E32_REGISTER_UGAINB, this->phase_[PHASEB].voltage_gain_); // B Voltage rms gain this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
this->write16_(ATM90E32_REGISTER_IGAINB, this->phase_[PHASEB].ct_gain_); // B line current gain this->restore_offset_calibrations_();
// Setup voltage and current gain for PHASE C
this->write16_(ATM90E32_REGISTER_UGAINC, this->phase_[PHASEC].voltage_gain_); // C Voltage rms gain // Initialize flash storage for power offset calibrations
this->write16_(ATM90E32_REGISTER_IGAINC, this->phase_[PHASEC].ct_gain_); // C line current gain uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
this->restore_power_offset_calibrations_();
} else {
ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(this->voltage_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
this->write16_(this->current_offset_registers[phase],
static_cast<uint16_t>(this->offset_phase_[phase].current_offset_));
this->write16_(this->power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].active_power_offset));
this->write16_(this->reactive_power_offset_registers[phase],
static_cast<uint16_t>(this->power_offset_phase_[phase].reactive_power_offset));
}
}
if (this->enable_gain_calibration_) {
// Initialize flash storage for gain calibration
uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
this->restore_gain_calibrations_();
if (this->using_saved_calibrations_) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
} else {
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
}
}
} else {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
}
}
// Sag threshold (78%)
uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f);
// Overvoltage threshold (122%)
uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f);
// Write to registers
this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
this->write16_(ATM90E32_REGISTER_OVTH, ovth);
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
} }
void ATM90E32Component::dump_config() { void ATM90E32Component::dump_config() {
@@ -257,6 +224,7 @@ void ATM90E32Component::dump_config() {
LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_); LOG_SENSOR(" ", "Current A", this->phase_[PHASEA].current_sensor_);
LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_); LOG_SENSOR(" ", "Power A", this->phase_[PHASEA].power_sensor_);
LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power A", this->phase_[PHASEA].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power A", this->phase_[PHASEA].apparent_power_sensor_);
LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_); LOG_SENSOR(" ", "PF A", this->phase_[PHASEA].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy A", this->phase_[PHASEA].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy A", this->phase_[PHASEA].reverse_active_energy_sensor_);
@@ -267,22 +235,24 @@ void ATM90E32Component::dump_config() {
LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_); LOG_SENSOR(" ", "Current B", this->phase_[PHASEB].current_sensor_);
LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_); LOG_SENSOR(" ", "Power B", this->phase_[PHASEB].power_sensor_);
LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power B", this->phase_[PHASEB].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power B", this->phase_[PHASEB].apparent_power_sensor_);
LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_); LOG_SENSOR(" ", "PF B", this->phase_[PHASEB].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy B", this->phase_[PHASEB].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy B", this->phase_[PHASEB].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEB].harmonic_active_power_sensor_); LOG_SENSOR(" ", "Harmonic Power B", this->phase_[PHASEB].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEB].phase_angle_sensor_); LOG_SENSOR(" ", "Phase Angle B", this->phase_[PHASEB].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEB].peak_current_sensor_); LOG_SENSOR(" ", "Peak Current B", this->phase_[PHASEB].peak_current_sensor_);
LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_); LOG_SENSOR(" ", "Voltage C", this->phase_[PHASEC].voltage_sensor_);
LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_); LOG_SENSOR(" ", "Current C", this->phase_[PHASEC].current_sensor_);
LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_); LOG_SENSOR(" ", "Power C", this->phase_[PHASEC].power_sensor_);
LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_); LOG_SENSOR(" ", "Reactive Power C", this->phase_[PHASEC].reactive_power_sensor_);
LOG_SENSOR(" ", "Apparent Power C", this->phase_[PHASEC].apparent_power_sensor_);
LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_); LOG_SENSOR(" ", "PF C", this->phase_[PHASEC].power_factor_sensor_);
LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_); LOG_SENSOR(" ", "Active Forward Energy C", this->phase_[PHASEC].forward_active_energy_sensor_);
LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_); LOG_SENSOR(" ", "Active Reverse Energy C", this->phase_[PHASEC].reverse_active_energy_sensor_);
LOG_SENSOR(" ", "Harmonic Power A", this->phase_[PHASEC].harmonic_active_power_sensor_); LOG_SENSOR(" ", "Harmonic Power C", this->phase_[PHASEC].harmonic_active_power_sensor_);
LOG_SENSOR(" ", "Phase Angle A", this->phase_[PHASEC].phase_angle_sensor_); LOG_SENSOR(" ", "Phase Angle C", this->phase_[PHASEC].phase_angle_sensor_);
LOG_SENSOR(" ", "Peak Current A", this->phase_[PHASEC].peak_current_sensor_); LOG_SENSOR(" ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
LOG_SENSOR(" ", "Frequency", this->freq_sensor_); LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_); LOG_SENSOR(" ", "Chip Temp", this->chip_temperature_sensor_);
} }
@@ -298,7 +268,7 @@ uint16_t ATM90E32Component::read16_(uint16_t a_register) {
uint8_t data[2]; uint8_t data[2];
uint16_t output; uint16_t output;
this->enable(); this->enable();
delay_microseconds_safe(10); delay_microseconds_safe(1); // min delay between CS low and first SCK is 200ns - 1ms is plenty
this->write_byte(addrh); this->write_byte(addrh);
this->write_byte(addrl); this->write_byte(addrl);
this->read_array(data, 2); this->read_array(data, 2);
@@ -328,8 +298,7 @@ void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
this->write_byte16(a_register); this->write_byte16(a_register);
this->write_byte16(val); this->write_byte16(val);
this->disable(); this->disable();
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != val) this->validate_spi_read_(val, "write16()");
ESP_LOGW(TAG, "SPI write error 0x%04X val 0x%04X", a_register, val);
} }
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; } float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
@@ -340,6 +309,8 @@ float ATM90E32Component::get_local_phase_active_power_(uint8_t phase) { return t
float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; } float ATM90E32Component::get_local_phase_reactive_power_(uint8_t phase) { return this->phase_[phase].reactive_power_; }
float ATM90E32Component::get_local_phase_apparent_power_(uint8_t phase) { return this->phase_[phase].apparent_power_; }
float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; } float ATM90E32Component::get_local_phase_power_factor_(uint8_t phase) { return this->phase_[phase].power_factor_; }
float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) { float ATM90E32Component::get_local_phase_forward_active_energy_(uint8_t phase) {
@@ -360,8 +331,7 @@ float ATM90E32Component::get_local_phase_peak_current_(uint8_t phase) { return t
float ATM90E32Component::get_phase_voltage_(uint8_t phase) { float ATM90E32Component::get_phase_voltage_(uint8_t phase) {
const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); const uint16_t voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) this->validate_spi_read_(voltage, "get_phase_voltage()");
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
return (float) voltage / 100; return (float) voltage / 100;
} }
@@ -371,8 +341,7 @@ float ATM90E32Component::get_phase_voltage_avg_(uint8_t phase) {
uint16_t voltage = 0; uint16_t voltage = 0;
for (uint8_t i = 0; i < reads; i++) { for (uint8_t i = 0; i < reads; i++) {
voltage = this->read16_(ATM90E32_REGISTER_URMS + phase); voltage = this->read16_(ATM90E32_REGISTER_URMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != voltage) this->validate_spi_read_(voltage, "get_phase_voltage_avg_()");
ESP_LOGW(TAG, "SPI URMS voltage register read error.");
accumulation += voltage; accumulation += voltage;
} }
voltage = accumulation / reads; voltage = accumulation / reads;
@@ -386,8 +355,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
uint16_t current = 0; uint16_t current = 0;
for (uint8_t i = 0; i < reads; i++) { for (uint8_t i = 0; i < reads; i++) {
current = this->read16_(ATM90E32_REGISTER_IRMS + phase); current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) this->validate_spi_read_(current, "get_phase_current_avg_()");
ESP_LOGW(TAG, "SPI IRMS current register read error.");
accumulation += current; accumulation += current;
} }
current = accumulation / reads; current = accumulation / reads;
@@ -397,8 +365,7 @@ float ATM90E32Component::get_phase_current_avg_(uint8_t phase) {
float ATM90E32Component::get_phase_current_(uint8_t phase) { float ATM90E32Component::get_phase_current_(uint8_t phase) {
const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase); const uint16_t current = this->read16_(ATM90E32_REGISTER_IRMS + phase);
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != current) this->validate_spi_read_(current, "get_phase_current_()");
ESP_LOGW(TAG, "SPI IRMS current register read error.");
return (float) current / 1000; return (float) current / 1000;
} }
@@ -412,11 +379,15 @@ float ATM90E32Component::get_phase_reactive_power_(uint8_t phase) {
return val * 0.00032f; return val * 0.00032f;
} }
float ATM90E32Component::get_phase_apparent_power_(uint8_t phase) {
const int val = this->read32_(ATM90E32_REGISTER_SMEAN + phase, ATM90E32_REGISTER_SMEANLSB + phase);
return val * 0.00032f;
}
float ATM90E32Component::get_phase_power_factor_(uint8_t phase) { float ATM90E32Component::get_phase_power_factor_(uint8_t phase) {
const int16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); uint16_t powerfactor = this->read16_(ATM90E32_REGISTER_PFMEAN + phase); // unsigned to compare to lastspidata
if (this->read16_(ATM90E32_REGISTER_LASTSPIDATA) != powerfactor) this->validate_spi_read_(powerfactor, "get_phase_power_factor_()");
ESP_LOGW(TAG, "SPI power factor read error."); return (float) ((int16_t) powerfactor) / 1000; // make it signed again
return (float) powerfactor / 1000;
} }
float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) { float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
@@ -426,17 +397,19 @@ float ATM90E32Component::get_phase_forward_active_energy_(uint8_t phase) {
} else { } else {
this->phase_[phase].cumulative_forward_active_energy_ = val; this->phase_[phase].cumulative_forward_active_energy_ = val;
} }
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * 10 / 3200); // 0.01CF resolution = 0.003125 Wh per count
return ((float) this->phase_[phase].cumulative_forward_active_energy_ * (10.0f / 3200.0f));
} }
float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) { float ATM90E32Component::get_phase_reverse_active_energy_(uint8_t phase) {
const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY); const uint16_t val = this->read16_(ATM90E32_REGISTER_ANENERGY + phase);
if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) { if (UINT32_MAX - this->phase_[phase].cumulative_reverse_active_energy_ > val) {
this->phase_[phase].cumulative_reverse_active_energy_ += val; this->phase_[phase].cumulative_reverse_active_energy_ += val;
} else { } else {
this->phase_[phase].cumulative_reverse_active_energy_ = val; this->phase_[phase].cumulative_reverse_active_energy_ = val;
} }
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * 10 / 3200); // 0.01CF resolution = 0.003125 Wh per count
return ((float) this->phase_[phase].cumulative_reverse_active_energy_ * (10.0f / 3200.0f));
} }
float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) { float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
@@ -446,15 +419,15 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
float ATM90E32Component::get_phase_angle_(uint8_t phase) { float ATM90E32Component::get_phase_angle_(uint8_t phase) {
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0; uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
return (float) (val > 180) ? val - 360.0 : val; return (val > 180) ? (float) (val - 360.0f) : (float) val;
} }
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) { float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase); int16_t val = (float) this->read16_(ATM90E32_REGISTER_IPEAK + phase);
if (!this->peak_current_signed_) if (!this->peak_current_signed_)
val = abs(val); val = std::abs(val);
// phase register * phase current gain value / 1000 * 2^13 // phase register * phase current gain value / 1000 * 2^13
return (float) (val * this->phase_[phase].ct_gain_ / 8192000.0); return (val * this->phase_[phase].ct_gain_ / 8192000.0);
} }
float ATM90E32Component::get_frequency_() { float ATM90E32Component::get_frequency_() {
@@ -467,29 +440,433 @@ float ATM90E32Component::get_chip_temperature_() {
return (float) ctemp; return (float) ctemp;
} }
uint16_t ATM90E32Component::calibrate_voltage_offset_phase(uint8_t phase) { void ATM90E32Component::run_gain_calibrations() {
const uint8_t num_reads = 5; if (!this->enable_gain_calibration_) {
uint64_t total_value = 0; ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
for (int i = 0; i < num_reads; ++i) { return;
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase);
total_value += measurement_value;
} }
const uint32_t average_value = total_value / num_reads;
const uint32_t shifted_value = average_value >> 7; float ref_voltages[3] = {
const uint32_t voltage_offset = ~shifted_value + 1; this->get_reference_voltage(0),
return voltage_offset & 0xFFFF; // Take the lower 16 bits this->get_reference_voltage(1),
this->get_reference_voltage(2),
};
float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
this->get_reference_current(2)};
ESP_LOGI(TAG, "[CALIBRATION] ");
ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration =========================");
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
ESP_LOGI(TAG,
"[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref | V_gain (old→new) | I_gain (old→new) |");
ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
for (uint8_t phase = 0; phase < 3; phase++) {
float measured_voltage = this->get_phase_voltage_avg_(phase);
float measured_current = this->get_phase_current_avg_(phase);
float ref_voltage = ref_voltages[phase];
float ref_current = ref_currents[phase];
uint16_t current_voltage_gain = this->read16_(voltage_gain_registers[phase]);
uint16_t current_current_gain = this->read16_(current_gain_registers[phase]);
bool did_voltage = false;
bool did_current = false;
// Voltage calibration
if (ref_voltage <= 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
phase_labels[phase]);
} else if (measured_voltage == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
phase_labels[phase]);
} else {
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
if (new_voltage_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
phase_labels[phase]);
} else {
if (new_voltage_gain >= 65535) {
ESP_LOGW(
TAG,
"[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
phase_labels[phase]);
new_voltage_gain = 65535;
}
this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
did_voltage = true;
}
}
// Current calibration
if (ref_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
phase_labels[phase]);
} else if (measured_current == 0.0f) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
phase_labels[phase]);
} else {
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
if (new_current_gain == 0) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
phase_labels[phase]);
} else {
if (new_current_gain >= 65535) {
ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
phase_labels[phase]);
new_current_gain = 65535;
}
this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
did_current = true;
}
}
// Final row output
ESP_LOGI(TAG, "[CALIBRATION] | %c | %9.2f | %9.4f | %5.2f | %6.4f | %5u → %-5u | %5u → %-5u |",
'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
}
ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
this->save_gain_calibration_to_memory_();
this->write_gains_to_registers_();
this->verify_gain_writes_();
} }
uint16_t ATM90E32Component::calibrate_current_offset_phase(uint8_t phase) { void ATM90E32Component::save_gain_calibration_to_memory_() {
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
if (success) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
}
}
void ATM90E32Component::run_offset_calibrations() {
if (!this->enable_offset_calibration_) {
ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
return;
}
for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = calibrate_offset(phase, true);
int16_t current_offset = calibrate_offset(phase, false);
this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
current_offset);
}
this->offset_pref_.save(&this->offset_phase_); // Save to flash
}
void ATM90E32Component::run_power_offset_calibrations() {
if (!this->enable_offset_calibration_) {
ESP_LOGW(
TAG,
"[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
return;
}
for (uint8_t phase = 0; phase < 3; ++phase) {
int16_t active_offset = calibrate_power_offset(phase, false);
int16_t reactive_offset = calibrate_power_offset(phase, true);
this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
active_offset, reactive_offset);
}
this->power_offset_pref_.save(&this->power_offset_phase_); // Save to flash
}
void ATM90E32Component::write_gains_to_registers_() {
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
for (int phase = 0; phase < 3; phase++) {
this->write16_(voltage_gain_registers[phase], this->gain_phase_[phase].voltage_gain);
this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain);
}
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset) {
// Save to runtime
this->offset_phase_[phase].voltage_offset_ = voltage_offset;
this->phase_[phase].voltage_offset_ = voltage_offset;
// Save to flash-storable struct
this->offset_phase_[phase].current_offset_ = current_offset;
this->phase_[phase].current_offset_ = current_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(voltage_offset_registers[phase], static_cast<uint16_t>(voltage_offset));
this->write16_(current_offset_registers[phase], static_cast<uint16_t>(current_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset) {
// Save to runtime
this->phase_[phase].active_power_offset_ = p_offset;
this->phase_[phase].reactive_power_offset_ = q_offset;
// Save to flash-storable struct
this->power_offset_phase_[phase].active_power_offset = p_offset;
this->power_offset_phase_[phase].reactive_power_offset = q_offset;
// Write to registers
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(this->power_offset_registers[phase], static_cast<uint16_t>(p_offset));
this->write16_(this->reactive_power_offset_registers[phase], static_cast<uint16_t>(q_offset));
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::restore_gain_calibrations_() {
if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
uint16_t i_gain = this->gain_phase_[phase].current_gain;
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
}
this->write_gains_to_registers_();
if (this->verify_gain_writes_()) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
}
} else {
this->using_saved_calibrations_ = false;
ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
}
}
void ATM90E32Component::restore_offset_calibrations_() {
if (this->offset_pref_.load(&this->offset_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
for (uint8_t phase = 0; phase < 3; phase++) {
auto &offset = this->offset_phase_[phase];
write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
offset.voltage_offset_, offset.current_offset_);
}
} else {
ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
}
}
void ATM90E32Component::restore_power_offset_calibrations_() {
if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
for (uint8_t phase = 0; phase < 3; ++phase) {
auto &offset = this->power_offset_phase_[phase];
write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
offset.active_power_offset, offset.reactive_power_offset);
}
} else {
ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
}
}
void ATM90E32Component::clear_gain_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values...");
for (int phase = 0; phase < 3; phase++) {
gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
}
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
this->using_saved_calibrations_ = false;
if (success) {
ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
for (int phase = 0; phase < 3; phase++) {
ESP_LOGI(TAG, "[CALIBRATION] Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
}
} else {
ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
}
this->write_gains_to_registers_(); // Apply them to the chip immediately
}
void ATM90E32Component::clear_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_offsets_to_registers_(phase, 0, 0);
}
this->offset_pref_.save(&this->offset_phase_); // Save cleared values to flash memory
ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
}
void ATM90E32Component::clear_power_offset_calibrations() {
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_power_offsets_to_registers_(phase, 0, 0);
}
this->power_offset_pref_.save(&this->power_offset_phase_);
ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
}
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
const uint8_t num_reads = 5; const uint8_t num_reads = 5;
uint64_t total_value = 0; uint64_t total_value = 0;
for (int i = 0; i < num_reads; ++i) {
const uint32_t measurement_value = read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase); for (uint8_t i = 0; i < num_reads; ++i) {
total_value += measurement_value; uint32_t reading = voltage ? this->read32_(ATM90E32_REGISTER_URMS + phase, ATM90E32_REGISTER_URMSLSB + phase)
: this->read32_(ATM90E32_REGISTER_IRMS + phase, ATM90E32_REGISTER_IRMSLSB + phase);
total_value += reading;
} }
const uint32_t average_value = total_value / num_reads; const uint32_t average_value = total_value / num_reads;
const uint32_t current_offset = ~average_value + 1; const uint32_t shifted = average_value >> 7;
return current_offset & 0xFFFF; // Take the lower 16 bits const uint32_t offset = ~shifted + 1;
return static_cast<int16_t>(offset); // Takes lower 16 bits
}
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
const uint8_t num_reads = 5;
uint64_t total_value = 0;
for (uint8_t i = 0; i < num_reads; ++i) {
uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
: this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
total_value += reading;
}
const uint32_t average_value = total_value / num_reads;
const uint32_t power_offset = ~average_value + 1;
return static_cast<int16_t>(power_offset); // Takes the lower 16 bits
}
bool ATM90E32Component::verify_gain_writes_() {
bool success = true;
for (uint8_t phase = 0; phase < 3; phase++) {
uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
uint16_t read_current = this->read16_(current_gain_registers[phase]);
if (read_voltage != this->gain_phase_[phase].voltage_gain ||
read_current != this->gain_phase_[phase].current_gain) {
ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
success = false;
}
}
return success; // Return true if all writes were successful, false otherwise
}
#ifdef USE_TEXT_SENSOR
void ATM90E32Component::check_phase_status() {
uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
for (int phase = 0; phase < 3; phase++) {
std::string status;
if (state0 & over_voltage_flags[phase])
status += "Over Voltage; ";
if (state1 & voltage_sag_flags[phase])
status += "Voltage Sag; ";
if (state1 & phase_loss_flags[phase])
status += "Phase Loss; ";
auto *sensor = this->phase_status_text_sensor_[phase];
const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
if (!status.empty()) {
status.pop_back(); // remove space
status.pop_back(); // remove semicolon
ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
if (sensor != nullptr)
sensor->publish_state(status);
} else {
if (sensor != nullptr)
sensor->publish_state("Okay");
}
}
}
void ATM90E32Component::check_freq_status() {
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
std::string freq_status;
if (state1 & ATM90E32_STATUS_S1_FREQHIST) {
freq_status = "HIGH";
} else if (state1 & ATM90E32_STATUS_S1_FREQLOST) {
freq_status = "LOW";
} else {
freq_status = "Normal";
}
ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
if (this->freq_status_text_sensor_ != nullptr) {
this->freq_status_text_sensor_->publish_state(freq_status);
}
}
void ATM90E32Component::check_over_current() {
constexpr float max_current_threshold = 65.53f;
for (uint8_t phase = 0; phase < 3; phase++) {
float current_val =
this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f;
if (current_val > max_current_threshold) {
ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val);
ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2");
if (this->phase_status_text_sensor_[phase] != nullptr) {
this->phase_status_text_sensor_[phase]->publish_state("Over Current; ");
}
}
}
}
#endif
uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) {
// this assumes that 60Hz electrical systems use 120V mains,
// which is usually, but not always the case
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
float target_voltage = nominal_voltage * multiplier;
float peak_01v = target_voltage * 100.0f * std::sqrt(2.0f); // convert RMS → peak, scale to 0.01V
float divider = (2.0f * ugain) / 32768.0f;
float threshold = peak_01v / divider;
return static_cast<uint16_t>(threshold);
}
bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) {
uint16_t last = this->read16_(ATM90E32_REGISTER_LASTSPIDATA);
if (last != expected) {
if (context != nullptr) {
ESP_LOGW(TAG, "[%s] SPI read mismatch: expected 0x%04X, got 0x%04X", context, expected, last);
} else {
ESP_LOGW(TAG, "SPI read mismatch: expected 0x%04X, got 0x%04X", expected, last);
}
return false;
}
return true;
} }
} // namespace atm90e32 } // namespace atm90e32

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <unordered_map>
#include "atm90e32_reg.h" #include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h" #include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h" #include "esphome/components/spi/spi.h"
@@ -18,6 +19,26 @@ class ATM90E32Component : public PollingComponent,
static const uint8_t PHASEA = 0; static const uint8_t PHASEA = 0;
static const uint8_t PHASEB = 1; static const uint8_t PHASEB = 1;
static const uint8_t PHASEC = 2; static const uint8_t PHASEC = 2;
const char *phase_labels[3] = {"A", "B", "C"};
// these registers are not sucessive, so we can't just do 'base + phase'
const uint16_t voltage_gain_registers[3] = {ATM90E32_REGISTER_UGAINA, ATM90E32_REGISTER_UGAINB,
ATM90E32_REGISTER_UGAINC};
const uint16_t current_gain_registers[3] = {ATM90E32_REGISTER_IGAINA, ATM90E32_REGISTER_IGAINB,
ATM90E32_REGISTER_IGAINC};
const uint16_t voltage_offset_registers[3] = {ATM90E32_REGISTER_UOFFSETA, ATM90E32_REGISTER_UOFFSETB,
ATM90E32_REGISTER_UOFFSETC};
const uint16_t current_offset_registers[3] = {ATM90E32_REGISTER_IOFFSETA, ATM90E32_REGISTER_IOFFSETB,
ATM90E32_REGISTER_IOFFSETC};
const uint16_t power_offset_registers[3] = {ATM90E32_REGISTER_POFFSETA, ATM90E32_REGISTER_POFFSETB,
ATM90E32_REGISTER_POFFSETC};
const uint16_t reactive_power_offset_registers[3] = {ATM90E32_REGISTER_QOFFSETA, ATM90E32_REGISTER_QOFFSETB,
ATM90E32_REGISTER_QOFFSETC};
const uint16_t over_voltage_flags[3] = {ATM90E32_STATUS_S0_OVPHASEAST, ATM90E32_STATUS_S0_OVPHASEBST,
ATM90E32_STATUS_S0_OVPHASECST};
const uint16_t voltage_sag_flags[3] = {ATM90E32_STATUS_S1_SAGPHASEAST, ATM90E32_STATUS_S1_SAGPHASEBST,
ATM90E32_STATUS_S1_SAGPHASECST};
const uint16_t phase_loss_flags[3] = {ATM90E32_STATUS_S1_PHASELOSSAST, ATM90E32_STATUS_S1_PHASELOSSBST,
ATM90E32_STATUS_S1_PHASELOSSCST};
void loop() override; void loop() override;
void setup() override; void setup() override;
void dump_config() override; void dump_config() override;
@@ -42,6 +63,14 @@ class ATM90E32Component : public PollingComponent,
void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; } void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; } void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; } void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
void set_active_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].active_power_offset = offset;
}
void set_reactive_power_offset(uint8_t phase, int16_t offset) {
this->power_offset_phase_[phase].reactive_power_offset = offset;
}
void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; } void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; } void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) { void set_chip_temperature_sensor(sensor::Sensor *chip_temperature_sensor) {
@@ -51,53 +80,104 @@ class ATM90E32Component : public PollingComponent,
void set_current_phases(int phases) { current_phases_ = phases; } void set_current_phases(int phases) { current_phases_ = phases; }
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; } void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
void run_offset_calibrations(); void run_offset_calibrations();
void run_power_offset_calibrations();
void clear_offset_calibrations(); void clear_offset_calibrations();
void clear_power_offset_calibrations();
void clear_gain_calibrations();
void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; } void set_enable_offset_calibration(bool flag) { enable_offset_calibration_ = flag; }
uint16_t calibrate_voltage_offset_phase(uint8_t /*phase*/); void set_enable_gain_calibration(bool flag) { enable_gain_calibration_ = flag; }
uint16_t calibrate_current_offset_phase(uint8_t /*phase*/); int16_t calibrate_offset(uint8_t phase, bool voltage);
int16_t calibrate_power_offset(uint8_t phase, bool reactive);
void run_gain_calibrations();
#ifdef USE_NUMBER
void set_reference_voltage(uint8_t phase, number::Number *ref_voltage) { ref_voltages_[phase] = ref_voltage; }
void set_reference_current(uint8_t phase, number::Number *ref_current) { ref_currents_[phase] = ref_current; }
#endif
float get_reference_voltage(uint8_t phase) {
#ifdef USE_NUMBER
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
#else
return 120.0; // Default voltage
#endif
}
float get_reference_current(uint8_t phase) {
#ifdef USE_NUMBER
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
#else
return 5.0f; // Default current
#endif
}
bool using_saved_calibrations_ = false; // Track if stored calibrations are being used
#ifdef USE_TEXT_SENSOR
void check_phase_status();
void check_freq_status();
void check_over_current();
void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) {
this->phase_status_text_sensor_[phase] = sensor;
}
void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
#endif
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
int32_t last_periodic_millis = millis(); int32_t last_periodic_millis = millis();
protected: protected:
#ifdef USE_NUMBER
number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr};
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
#endif
uint16_t read16_(uint16_t a_register); uint16_t read16_(uint16_t a_register);
int read32_(uint16_t addr_h, uint16_t addr_l); int read32_(uint16_t addr_h, uint16_t addr_l);
void write16_(uint16_t a_register, uint16_t val); void write16_(uint16_t a_register, uint16_t val);
float get_local_phase_voltage_(uint8_t /*phase*/); float get_local_phase_voltage_(uint8_t phase);
float get_local_phase_current_(uint8_t /*phase*/); float get_local_phase_current_(uint8_t phase);
float get_local_phase_active_power_(uint8_t /*phase*/); float get_local_phase_active_power_(uint8_t phase);
float get_local_phase_reactive_power_(uint8_t /*phase*/); float get_local_phase_reactive_power_(uint8_t phase);
float get_local_phase_power_factor_(uint8_t /*phase*/); float get_local_phase_apparent_power_(uint8_t phase);
float get_local_phase_forward_active_energy_(uint8_t /*phase*/); float get_local_phase_power_factor_(uint8_t phase);
float get_local_phase_reverse_active_energy_(uint8_t /*phase*/); float get_local_phase_forward_active_energy_(uint8_t phase);
float get_local_phase_angle_(uint8_t /*phase*/); float get_local_phase_reverse_active_energy_(uint8_t phase);
float get_local_phase_harmonic_active_power_(uint8_t /*phase*/); float get_local_phase_angle_(uint8_t phase);
float get_local_phase_peak_current_(uint8_t /*phase*/); float get_local_phase_harmonic_active_power_(uint8_t phase);
float get_phase_voltage_(uint8_t /*phase*/); float get_local_phase_peak_current_(uint8_t phase);
float get_phase_voltage_avg_(uint8_t /*phase*/); float get_phase_voltage_(uint8_t phase);
float get_phase_current_(uint8_t /*phase*/); float get_phase_voltage_avg_(uint8_t phase);
float get_phase_current_avg_(uint8_t /*phase*/); float get_phase_current_(uint8_t phase);
float get_phase_active_power_(uint8_t /*phase*/); float get_phase_current_avg_(uint8_t phase);
float get_phase_reactive_power_(uint8_t /*phase*/); float get_phase_active_power_(uint8_t phase);
float get_phase_power_factor_(uint8_t /*phase*/); float get_phase_reactive_power_(uint8_t phase);
float get_phase_forward_active_energy_(uint8_t /*phase*/); float get_phase_apparent_power_(uint8_t phase);
float get_phase_reverse_active_energy_(uint8_t /*phase*/); float get_phase_power_factor_(uint8_t phase);
float get_phase_angle_(uint8_t /*phase*/); float get_phase_forward_active_energy_(uint8_t phase);
float get_phase_harmonic_active_power_(uint8_t /*phase*/); float get_phase_reverse_active_energy_(uint8_t phase);
float get_phase_peak_current_(uint8_t /*phase*/); float get_phase_angle_(uint8_t phase);
float get_phase_harmonic_active_power_(uint8_t phase);
float get_phase_peak_current_(uint8_t phase);
float get_frequency_(); float get_frequency_();
float get_chip_temperature_(); float get_chip_temperature_();
bool get_publish_interval_flag_() { return publish_interval_flag_; }; bool get_publish_interval_flag_() { return publish_interval_flag_; };
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; }; void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
void restore_calibrations_(); void restore_offset_calibrations_();
void restore_power_offset_calibrations_();
void restore_gain_calibrations_();
void save_gain_calibration_to_memory_();
void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
void write_gains_to_registers_();
bool verify_gain_writes_();
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
struct ATM90E32Phase { struct ATM90E32Phase {
uint16_t voltage_gain_{0}; uint16_t voltage_gain_{0};
uint16_t ct_gain_{0}; uint16_t ct_gain_{0};
uint16_t voltage_offset_{0}; int16_t voltage_offset_{0};
uint16_t current_offset_{0}; int16_t current_offset_{0};
int16_t active_power_offset_{0};
int16_t reactive_power_offset_{0};
float voltage_{0}; float voltage_{0};
float current_{0}; float current_{0};
float active_power_{0}; float active_power_{0};
float reactive_power_{0}; float reactive_power_{0};
float apparent_power_{0};
float power_factor_{0}; float power_factor_{0};
float forward_active_energy_{0}; float forward_active_energy_{0};
float reverse_active_energy_{0}; float reverse_active_energy_{0};
@@ -119,14 +199,30 @@ class ATM90E32Component : public PollingComponent,
uint32_t cumulative_reverse_active_energy_{0}; uint32_t cumulative_reverse_active_energy_{0};
} phase_[3]; } phase_[3];
struct Calibration { struct OffsetCalibration {
uint16_t voltage_offset_{0}; int16_t voltage_offset_{0};
uint16_t current_offset_{0}; int16_t current_offset_{0};
} offset_phase_[3]; } offset_phase_[3];
ESPPreferenceObject pref_; struct PowerOffsetCalibration {
int16_t active_power_offset{0};
int16_t reactive_power_offset{0};
} power_offset_phase_[3];
struct GainCalibration {
uint16_t voltage_gain{1};
uint16_t current_gain{1};
} gain_phase_[3];
ESPPreferenceObject offset_pref_;
ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_;
sensor::Sensor *freq_sensor_{nullptr}; sensor::Sensor *freq_sensor_{nullptr};
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr};
text_sensor::TextSensor *freq_status_text_sensor_{nullptr};
#endif
sensor::Sensor *chip_temperature_sensor_{nullptr}; sensor::Sensor *chip_temperature_sensor_{nullptr};
uint16_t pga_gain_{0x15}; uint16_t pga_gain_{0x15};
int line_freq_{60}; int line_freq_{60};
@@ -134,6 +230,7 @@ class ATM90E32Component : public PollingComponent,
bool publish_interval_flag_{false}; bool publish_interval_flag_{false};
bool peak_current_signed_{false}; bool peak_current_signed_{false};
bool enable_offset_calibration_{false}; bool enable_offset_calibration_{false};
bool enable_gain_calibration_{false};
}; };
} // namespace atm90e32 } // namespace atm90e32

View File

@@ -176,16 +176,17 @@ static const uint16_t ATM90E32_REGISTER_ANENERGYCH = 0xAF; // C Reverse Harm. E
/* POWER & P.F. REGISTERS */ /* POWER & P.F. REGISTERS */
static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANT = 0xB0; // Total Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Mean Power Reg Base (P) static const uint16_t ATM90E32_REGISTER_PMEAN = 0xB1; // Active Power Reg Base (P)
static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANA = 0xB1; // A Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANB = 0xB2; // B Mean Power (P)
static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P) static const uint16_t ATM90E32_REGISTER_PMEANC = 0xB3; // C Mean Power (P)
static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANT = 0xB4; // Total Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Mean Power Reg Base (Q) static const uint16_t ATM90E32_REGISTER_QMEAN = 0xB5; // Reactive Power Reg Base (Q)
static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANA = 0xB5; // A Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANB = 0xB6; // B Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q) static const uint16_t ATM90E32_REGISTER_QMEANC = 0xB7; // C Mean Power (Q)
static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANT = 0xB8; // Total Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEAN = 0xB9; // Apparent Mean Power Base (S)
static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANA = 0xB9; // A Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANB = 0xBA; // B Mean Power (S)
static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S) static const uint16_t ATM90E32_REGISTER_SMEANC = 0xBB; // C Mean Power (S)
@@ -206,6 +207,7 @@ static const uint16_t ATM90E32_REGISTER_QMEANALSB = 0xC5; // Lower Word (A Rea
static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power) static const uint16_t ATM90E32_REGISTER_QMEANBLSB = 0xC6; // Lower Word (B React. Power)
static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power) static const uint16_t ATM90E32_REGISTER_QMEANCLSB = 0xC7; // Lower Word (C React. Power)
static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power) static const uint16_t ATM90E32_REGISTER_SAMEANTLSB = 0xC8; // Lower Word (Tot. App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANLSB = 0xC9; // Lower Word Reg Base (Apparent Power)
static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power) static const uint16_t ATM90E32_REGISTER_SMEANALSB = 0xC9; // Lower Word (A App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power) static const uint16_t ATM90E32_REGISTER_SMEANBLSB = 0xCA; // Lower Word (B App. Power)
static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power) static const uint16_t ATM90E32_REGISTER_SMEANCLSB = 0xCB; // Lower Word (C App. Power)

View File

@@ -1,43 +1,95 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import button from esphome.components import button
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_CHIP, ICON_SCALE from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from .. import atm90e32_ns from .. import atm90e32_ns
from ..sensor import ATM90E32Component from ..sensor import ATM90E32Component
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration"
CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration" CONF_RUN_OFFSET_CALIBRATION = "run_offset_calibration"
CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration" CONF_CLEAR_OFFSET_CALIBRATION = "clear_offset_calibration"
CONF_RUN_POWER_OFFSET_CALIBRATION = "run_power_offset_calibration"
CONF_CLEAR_POWER_OFFSET_CALIBRATION = "clear_power_offset_calibration"
ATM90E32CalibrationButton = atm90e32_ns.class_( ATM90E32GainCalibrationButton = atm90e32_ns.class_(
"ATM90E32CalibrationButton", "ATM90E32GainCalibrationButton", button.Button
button.Button,
) )
ATM90E32ClearCalibrationButton = atm90e32_ns.class_( ATM90E32ClearGainCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearCalibrationButton", "ATM90E32ClearGainCalibrationButton", button.Button
button.Button, )
ATM90E32OffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32OffsetCalibrationButton", button.Button
)
ATM90E32ClearOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearOffsetCalibrationButton", button.Button
)
ATM90E32PowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32PowerOffsetCalibrationButton", button.Button
)
ATM90E32ClearPowerOffsetCalibrationButton = atm90e32_ns.class_(
"ATM90E32ClearPowerOffsetCalibrationButton", button.Button
) )
CONFIG_SCHEMA = { CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component), cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_RUN_GAIN_CALIBRATION): button.button_schema(
ATM90E32GainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:scale-balance",
),
cv.Optional(CONF_CLEAR_GAIN_CALIBRATION): button.button_schema(
ATM90E32ClearGainCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
),
cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema( cv.Optional(CONF_RUN_OFFSET_CALIBRATION): button.button_schema(
ATM90E32CalibrationButton, ATM90E32OffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE, icon=ICON_SCALE,
), ),
cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema( cv.Optional(CONF_CLEAR_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearCalibrationButton, ATM90E32ClearOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG, entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP, icon="mdi:delete",
),
cv.Optional(CONF_RUN_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32PowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_SCALE,
),
cv.Optional(CONF_CLEAR_POWER_OFFSET_CALIBRATION): button.button_schema(
ATM90E32ClearPowerOffsetCalibrationButton,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:delete",
), ),
} }
async def to_code(config): async def to_code(config):
parent = await cg.get_variable(config[CONF_ID]) parent = await cg.get_variable(config[CONF_ID])
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
b = await button.new_button(run_gain)
await cg.register_parented(b, parent)
if clear_gain := config.get(CONF_CLEAR_GAIN_CALIBRATION):
b = await button.new_button(clear_gain)
await cg.register_parented(b, parent)
if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION): if run_offset := config.get(CONF_RUN_OFFSET_CALIBRATION):
b = await button.new_button(run_offset) b = await button.new_button(run_offset)
await cg.register_parented(b, parent) await cg.register_parented(b, parent)
if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION): if clear_offset := config.get(CONF_CLEAR_OFFSET_CALIBRATION):
b = await button.new_button(clear_offset) b = await button.new_button(clear_offset)
await cg.register_parented(b, parent) await cg.register_parented(b, parent)
if run_power := config.get(CONF_RUN_POWER_OFFSET_CALIBRATION):
b = await button.new_button(run_power)
await cg.register_parented(b, parent)
if clear_power := config.get(CONF_CLEAR_POWER_OFFSET_CALIBRATION):
b = await button.new_button(clear_power)
await cg.register_parented(b, parent)

View File

@@ -1,4 +1,5 @@
#include "atm90e32_button.h" #include "atm90e32_button.h"
#include "esphome/core/component.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
namespace esphome { namespace esphome {
@@ -6,15 +7,73 @@ namespace atm90e32 {
static const char *const TAG = "atm90e32.button"; static const char *const TAG = "atm90e32.button";
void ATM90E32CalibrationButton::press_action() { void ATM90E32GainCalibrationButton::press_action() {
ESP_LOGI(TAG, "Running offset calibrations, Note: CTs and ACVs must be 0 during this process..."); if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Gain Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG,
"[CALIBRATION] Use gain_ct: & gain_voltage: under each phase_x: in your config file to save these values");
this->parent_->run_gain_calibrations();
}
void ATM90E32ClearGainCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Gain button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_gain_calibrations();
}
void ATM90E32OffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Offset Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs and ACVs must be 0 during this process. USB power only**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_voltage: & offset_current: under each phase_x: in your config file to save "
"these values");
this->parent_->run_offset_calibrations(); this->parent_->run_offset_calibrations();
} }
void ATM90E32ClearCalibrationButton::press_action() { void ATM90E32ClearOffsetCalibrationButton::press_action() {
ESP_LOGI(TAG, "Offset calibrations cleared."); if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Offset button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_offset_calibrations(); this->parent_->clear_offset_calibrations();
} }
void ATM90E32PowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Power Calibration button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
ESP_LOGI(TAG, "[CALIBRATION] **NOTE: CTs must be 0 during this process. Voltage reference should be present**");
ESP_LOGI(TAG, "[CALIBRATION] Use offset_active_power: & offset_reactive_power: under each phase_x: in your config "
"file to save these values");
this->parent_->run_power_offset_calibrations();
}
void ATM90E32ClearPowerOffsetCalibrationButton::press_action() {
if (this->parent_ == nullptr) {
ESP_LOGW(TAG, "[CALIBRATION] No meters assigned to Clear Power button [%s]", this->get_name().c_str());
return;
}
ESP_LOGI(TAG, "%s", this->get_name().c_str());
this->parent_->clear_power_offset_calibrations();
}
} // namespace atm90e32 } // namespace atm90e32
} // namespace esphome } // namespace esphome

View File

@@ -7,17 +7,49 @@
namespace esphome { namespace esphome {
namespace atm90e32 { namespace atm90e32 {
class ATM90E32CalibrationButton : public button::Button, public Parented<ATM90E32Component> { class ATM90E32GainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public: public:
ATM90E32CalibrationButton() = default; ATM90E32GainCalibrationButton() = default;
protected: protected:
void press_action() override; void press_action() override;
}; };
class ATM90E32ClearCalibrationButton : public button::Button, public Parented<ATM90E32Component> { class ATM90E32ClearGainCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public: public:
ATM90E32ClearCalibrationButton() = default; ATM90E32ClearGainCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32OffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32OffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32PowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32PowerOffsetCalibrationButton() = default;
protected:
void press_action() override;
};
class ATM90E32ClearPowerOffsetCalibrationButton : public button::Button, public Parented<ATM90E32Component> {
public:
ATM90E32ClearPowerOffsetCalibrationButton() = default;
protected: protected:
void press_action() override; void press_action() override;

View File

@@ -0,0 +1,130 @@
import esphome.codegen as cg
from esphome.components import number
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_MAX_VALUE,
CONF_MIN_VALUE,
CONF_MODE,
CONF_PHASE_A,
CONF_PHASE_B,
CONF_PHASE_C,
CONF_REFERENCE_VOLTAGE,
CONF_STEP,
ENTITY_CATEGORY_CONFIG,
UNIT_AMPERE,
UNIT_VOLT,
)
from .. import atm90e32_ns
from ..sensor import ATM90E32Component
ATM90E32Number = atm90e32_ns.class_(
"ATM90E32Number", number.Number, cg.Parented.template(ATM90E32Component)
)
CONF_REFERENCE_CURRENT = "reference_current"
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
REFERENCE_VOLTAGE_PHASE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_MODE, default="box"): cv.string,
cv.Optional(CONF_MIN_VALUE, default=100.0): cv.float_,
cv.Optional(CONF_MAX_VALUE, default=260.0): cv.float_,
cv.Optional(CONF_STEP, default=0.1): cv.float_,
}
).extend(
number.number_schema(
class_=ATM90E32Number,
unit_of_measurement=UNIT_VOLT,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:power-plug",
)
)
)
REFERENCE_CURRENT_PHASE_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_MODE, default="box"): cv.string,
cv.Optional(CONF_MIN_VALUE, default=1.0): cv.float_,
cv.Optional(CONF_MAX_VALUE, default=200.0): cv.float_,
cv.Optional(CONF_STEP, default=0.1): cv.float_,
}
).extend(
number.number_schema(
class_=ATM90E32Number,
unit_of_measurement=UNIT_AMPERE,
entity_category=ENTITY_CATEGORY_CONFIG,
icon="mdi:home-lightning-bolt",
)
)
)
REFERENCE_VOLTAGE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): REFERENCE_VOLTAGE_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_B): REFERENCE_VOLTAGE_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_C): REFERENCE_VOLTAGE_PHASE_SCHEMA,
}
)
REFERENCE_CURRENT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): REFERENCE_CURRENT_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_B): REFERENCE_CURRENT_PHASE_SCHEMA,
cv.Optional(CONF_PHASE_C): REFERENCE_CURRENT_PHASE_SCHEMA,
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(ATM90E32Component),
cv.Optional(CONF_REFERENCE_VOLTAGE): REFERENCE_VOLTAGE_SCHEMA,
cv.Optional(CONF_REFERENCE_CURRENT): REFERENCE_CURRENT_SCHEMA,
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if voltage_cfg := config.get(CONF_REFERENCE_VOLTAGE):
voltage_objs = [None, None, None]
for i, key in enumerate(PHASE_KEYS):
if validated := voltage_cfg.get(key):
obj = await number.new_number(
validated,
min_value=validated["min_value"],
max_value=validated["max_value"],
step=validated["step"],
)
await cg.register_parented(obj, parent)
voltage_objs[i] = obj
# Inherit from A → B/C if only A defined
if voltage_objs[0] is not None:
for i in range(3):
if voltage_objs[i] is None:
voltage_objs[i] = voltage_objs[0]
for i, obj in enumerate(voltage_objs):
if obj is not None:
cg.add(parent.set_reference_voltage(i, obj))
if current_cfg := config.get(CONF_REFERENCE_CURRENT):
for i, key in enumerate(PHASE_KEYS):
if validated := current_cfg.get(key):
obj = await number.new_number(
validated,
min_value=validated["min_value"],
max_value=validated["max_value"],
step=validated["step"],
)
await cg.register_parented(obj, parent)
cg.add(parent.set_reference_current(i, obj))

View File

@@ -0,0 +1,16 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/atm90e32/atm90e32.h"
#include "esphome/components/number/number.h"
namespace esphome {
namespace atm90e32 {
class ATM90E32Number : public number::Number, public Parented<ATM90E32Component> {
public:
void control(float value) override { this->publish_state(value); }
};
} // namespace atm90e32
} // namespace esphome

View File

@@ -33,6 +33,7 @@ from esphome.const import (
UNIT_DEGREES, UNIT_DEGREES,
UNIT_HERTZ, UNIT_HERTZ,
UNIT_VOLT, UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE, UNIT_VOLT_AMPS_REACTIVE,
UNIT_WATT, UNIT_WATT,
UNIT_WATT_HOURS, UNIT_WATT_HOURS,
@@ -45,10 +46,17 @@ CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases" CONF_CURRENT_PHASES = "current_phases"
CONF_GAIN_VOLTAGE = "gain_voltage" CONF_GAIN_VOLTAGE = "gain_voltage"
CONF_GAIN_CT = "gain_ct" CONF_GAIN_CT = "gain_ct"
CONF_OFFSET_VOLTAGE = "offset_voltage"
CONF_OFFSET_CURRENT = "offset_current"
CONF_OFFSET_ACTIVE_POWER = "offset_active_power"
CONF_OFFSET_REACTIVE_POWER = "offset_reactive_power"
CONF_HARMONIC_POWER = "harmonic_power" CONF_HARMONIC_POWER = "harmonic_power"
CONF_PEAK_CURRENT = "peak_current" CONF_PEAK_CURRENT = "peak_current"
CONF_PEAK_CURRENT_SIGNED = "peak_current_signed" CONF_PEAK_CURRENT_SIGNED = "peak_current_signed"
CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration" CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration"
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
UNIT_DEG = "degrees" UNIT_DEG = "degrees"
LINE_FREQS = { LINE_FREQS = {
"50HZ": 50, "50HZ": 50,
@@ -92,10 +100,11 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE, unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
icon=ICON_LIGHTBULB, icon=ICON_LIGHTBULB,
accuracy_decimals=2, accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
), ),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema( cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT, unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=2, accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER, device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT, state_class=STATE_CLASS_MEASUREMENT,
@@ -137,6 +146,10 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
), ),
cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t, cv.Optional(CONF_GAIN_VOLTAGE, default=7305): cv.uint16_t,
cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t, cv.Optional(CONF_GAIN_CT, default=27961): cv.uint16_t,
cv.Optional(CONF_OFFSET_VOLTAGE, default=0): cv.int_,
cv.Optional(CONF_OFFSET_CURRENT, default=0): cv.int_,
cv.Optional(CONF_OFFSET_ACTIVE_POWER, default=0): cv.int_,
cv.Optional(CONF_OFFSET_REACTIVE_POWER, default=0): cv.int_,
} }
) )
@@ -164,9 +177,10 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum( cv.Optional(CONF_CURRENT_PHASES, default="3"): cv.enum(
CURRENT_PHASES, upper=True CURRENT_PHASES, upper=True
), ),
cv.Optional(CONF_GAIN_PGA, default="2X"): cv.enum(PGA_GAINS, upper=True), cv.Optional(CONF_GAIN_PGA, default="1X"): cv.enum(PGA_GAINS, upper=True),
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean, cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean, cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean,
} }
) )
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
@@ -185,6 +199,10 @@ async def to_code(config):
conf = config[phase] conf = config[phase]
cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE])) cg.add(var.set_volt_gain(i, conf[CONF_GAIN_VOLTAGE]))
cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT])) cg.add(var.set_ct_gain(i, conf[CONF_GAIN_CT]))
cg.add(var.set_voltage_offset(i, conf[CONF_OFFSET_VOLTAGE]))
cg.add(var.set_current_offset(i, conf[CONF_OFFSET_CURRENT]))
cg.add(var.set_active_power_offset(i, conf[CONF_OFFSET_ACTIVE_POWER]))
cg.add(var.set_reactive_power_offset(i, conf[CONF_OFFSET_REACTIVE_POWER]))
if voltage_config := conf.get(CONF_VOLTAGE): if voltage_config := conf.get(CONF_VOLTAGE):
sens = await sensor.new_sensor(voltage_config) sens = await sensor.new_sensor(voltage_config)
cg.add(var.set_voltage_sensor(i, sens)) cg.add(var.set_voltage_sensor(i, sens))
@@ -218,16 +236,15 @@ async def to_code(config):
if peak_current_config := conf.get(CONF_PEAK_CURRENT): if peak_current_config := conf.get(CONF_PEAK_CURRENT):
sens = await sensor.new_sensor(peak_current_config) sens = await sensor.new_sensor(peak_current_config)
cg.add(var.set_peak_current_sensor(i, sens)) cg.add(var.set_peak_current_sensor(i, sens))
if frequency_config := config.get(CONF_FREQUENCY): if frequency_config := config.get(CONF_FREQUENCY):
sens = await sensor.new_sensor(frequency_config) sens = await sensor.new_sensor(frequency_config)
cg.add(var.set_freq_sensor(sens)) cg.add(var.set_freq_sensor(sens))
if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE): if chip_temperature_config := config.get(CONF_CHIP_TEMPERATURE):
sens = await sensor.new_sensor(chip_temperature_config) sens = await sensor.new_sensor(chip_temperature_config)
cg.add(var.set_chip_temperature_sensor(sens)) cg.add(var.set_chip_temperature_sensor(sens))
cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY])) cg.add(var.set_line_freq(config[CONF_LINE_FREQUENCY]))
cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES])) cg.add(var.set_current_phases(config[CONF_CURRENT_PHASES]))
cg.add(var.set_pga_gain(config[CONF_GAIN_PGA])) cg.add(var.set_pga_gain(config[CONF_GAIN_PGA]))
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED])) cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION])) cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))
cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION]))

View File

@@ -0,0 +1,48 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
from ..sensor import ATM90E32Component
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
PHASE_KEYS = [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]
PHASE_STATUS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_PHASE_A): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
cv.Optional(CONF_PHASE_B): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
cv.Optional(CONF_PHASE_C): text_sensor.text_sensor_schema(
icon="mdi:flash-alert"
),
}
)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(ATM90E32Component),
cv.Optional(CONF_PHASE_STATUS): PHASE_STATUS_SCHEMA,
cv.Optional(CONF_FREQUENCY_STATUS): text_sensor.text_sensor_schema(
icon="mdi:lightbulb-alert"
),
}
)
async def to_code(config):
parent = await cg.get_variable(config[CONF_ID])
if phase_cfg := config.get(CONF_PHASE_STATUS):
for i, key in enumerate(PHASE_KEYS):
if sub_phase_cfg := phase_cfg.get(key):
sens = await text_sensor.new_text_sensor(sub_phase_cfg)
cg.add(parent.set_phase_status_text_sensor(i, sens))
if freq_status_config := config.get(CONF_FREQUENCY_STATUS):
sens = await text_sensor.new_text_sensor(freq_status_config)
cg.add(parent.set_freq_status_text_sensor(sens))

View File

@@ -37,16 +37,13 @@ AUDIO_COMPONENT_SCHEMA = cv.Schema(
) )
_UNDEF = object()
def set_stream_limits( def set_stream_limits(
min_bits_per_sample: int = _UNDEF, min_bits_per_sample: int = cv.UNDEFINED,
max_bits_per_sample: int = _UNDEF, max_bits_per_sample: int = cv.UNDEFINED,
min_channels: int = _UNDEF, min_channels: int = cv.UNDEFINED,
max_channels: int = _UNDEF, max_channels: int = cv.UNDEFINED,
min_sample_rate: int = _UNDEF, min_sample_rate: int = cv.UNDEFINED,
max_sample_rate: int = _UNDEF, max_sample_rate: int = cv.UNDEFINED,
): ):
"""Sets the limits for the audio stream that audio component can handle """Sets the limits for the audio stream that audio component can handle
@@ -55,17 +52,17 @@ def set_stream_limits(
""" """
def set_limits_in_config(config): def set_limits_in_config(config):
if min_bits_per_sample is not _UNDEF: if min_bits_per_sample is not cv.UNDEFINED:
config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample config[CONF_MIN_BITS_PER_SAMPLE] = min_bits_per_sample
if max_bits_per_sample is not _UNDEF: if max_bits_per_sample is not cv.UNDEFINED:
config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample config[CONF_MAX_BITS_PER_SAMPLE] = max_bits_per_sample
if min_channels is not _UNDEF: if min_channels is not cv.UNDEFINED:
config[CONF_MIN_CHANNELS] = min_channels config[CONF_MIN_CHANNELS] = min_channels
if max_channels is not _UNDEF: if max_channels is not cv.UNDEFINED:
config[CONF_MAX_CHANNELS] = max_channels config[CONF_MAX_CHANNELS] = max_channels
if min_sample_rate is not _UNDEF: if min_sample_rate is not cv.UNDEFINED:
config[CONF_MIN_SAMPLE_RATE] = min_sample_rate config[CONF_MIN_SAMPLE_RATE] = min_sample_rate
if max_sample_rate is not _UNDEF: if max_sample_rate is not cv.UNDEFINED:
config[CONF_MAX_SAMPLE_RATE] = max_sample_rate config[CONF_MAX_SAMPLE_RATE] = max_sample_rate
return set_limits_in_config return set_limits_in_config
@@ -75,10 +72,10 @@ def final_validate_audio_schema(
name: str, name: str,
*, *,
audio_device: str, audio_device: str,
bits_per_sample: int = _UNDEF, bits_per_sample: int = cv.UNDEFINED,
channels: int = _UNDEF, channels: int = cv.UNDEFINED,
sample_rate: int = _UNDEF, sample_rate: int = cv.UNDEFINED,
enabled_channels: list[int] = _UNDEF, enabled_channels: list[int] = cv.UNDEFINED,
audio_device_issue: bool = False, audio_device_issue: bool = False,
): ):
"""Validates audio compatibility when passed between different components. """Validates audio compatibility when passed between different components.
@@ -101,7 +98,7 @@ def final_validate_audio_schema(
def validate_audio_compatiblity(audio_config): def validate_audio_compatiblity(audio_config):
audio_schema = {} audio_schema = {}
if bits_per_sample is not _UNDEF: if bits_per_sample is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE), min=audio_config.get(CONF_MIN_BITS_PER_SAMPLE),
@@ -114,7 +111,7 @@ def final_validate_audio_schema(
error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}" error_string = f"Invalid configuration for the {name} component. The {CONF_BITS_PER_SAMPLE} {str(exc)}"
raise cv.Invalid(error_string) from exc raise cv.Invalid(error_string) from exc
if channels is not _UNDEF: if channels is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_CHANNELS), min=audio_config.get(CONF_MIN_CHANNELS),
@@ -127,7 +124,7 @@ def final_validate_audio_schema(
error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}" error_string = f"Invalid configuration for the {name} component. The {CONF_NUM_CHANNELS} {str(exc)}"
raise cv.Invalid(error_string) from exc raise cv.Invalid(error_string) from exc
if sample_rate is not _UNDEF: if sample_rate is not cv.UNDEFINED:
try: try:
cv.int_range( cv.int_range(
min=audio_config.get(CONF_MIN_SAMPLE_RATE), min=audio_config.get(CONF_MIN_SAMPLE_RATE),
@@ -140,7 +137,7 @@ def final_validate_audio_schema(
error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}" error_string = f"Invalid configuration for the {name} component. The {CONF_SAMPLE_RATE} {str(exc)}"
raise cv.Invalid(error_string) from exc raise cv.Invalid(error_string) from exc
if enabled_channels is not _UNDEF: if enabled_channels is not cv.UNDEFINED:
for channel in enabled_channels: for channel in enabled_channels:
try: try:
# Channels are 0-indexed # Channels are 0-indexed
@@ -168,4 +165,4 @@ def final_validate_audio_schema(
async def to_code(config): async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.3") cg.add_library("esphome/esp-audio-libs", "1.1.4")

View File

@@ -135,7 +135,7 @@ const char *audio_file_type_to_string(AudioFileType file_type);
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor, void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
size_t samples_to_scale); size_t samples_to_scale);
/// @brief Unpacks a quantized audio sample into a Q31 fixed point number. /// @brief Unpacks a quantized audio sample into a Q31 fixed-point number.
/// @param data Pointer to uint8_t array containing the audio sample /// @param data Pointer to uint8_t array containing the audio sample
/// @param bytes_per_sample The number of bytes per sample /// @param bytes_per_sample The number of bytes per sample
/// @return Q31 sample /// @return Q31 sample
@@ -160,5 +160,28 @@ inline int32_t unpack_audio_sample_to_q31(const uint8_t *data, size_t bytes_per_
return sample; return sample;
} }
/// @brief Packs a Q31 fixed-point number as an audio sample with the specified number of bytes per sample.
/// Packs the most significant bits - no dithering is applied.
/// @param sample Q31 fixed-point number to pack
/// @param data Pointer to data array to store
/// @param bytes_per_sample The audio data's bytes per sample
inline void pack_q31_as_audio_sample(int32_t sample, uint8_t *data, size_t bytes_per_sample) {
if (bytes_per_sample == 1) {
data[0] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 2) {
data[0] = static_cast<uint8_t>(sample >> 16);
data[1] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 3) {
data[0] = static_cast<uint8_t>(sample >> 8);
data[1] = static_cast<uint8_t>(sample >> 16);
data[2] = static_cast<uint8_t>(sample >> 24);
} else if (bytes_per_sample == 4) {
data[0] = static_cast<uint8_t>(sample);
data[1] = static_cast<uint8_t>(sample >> 8);
data[2] = static_cast<uint8_t>(sample >> 16);
data[3] = static_cast<uint8_t>(sample >> 24);
}
}
} // namespace audio } // namespace audio
} // namespace esphome } // namespace esphome

View File

@@ -171,7 +171,7 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
bytes_available_before_processing = this->input_transfer_buffer_->available(); bytes_available_before_processing = this->input_transfer_buffer_->available();
if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) { if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data // Failed to decode in last attempt and there is no new data
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) { if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {

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

@@ -1,31 +1,22 @@
import logging
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ID
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"]
BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent)
CONFIG_SCHEMA = ( CONFIG_SCHEMA = (
fan.FAN_SCHEMA.extend( fan.fan_schema(BedJetFan)
{
cv.GenerateID(): cv.declare_id(BedJetFan),
}
)
.extend(cv.polling_component_schema("60s")) .extend(cv.polling_component_schema("60s"))
.extend(BEDJET_CLIENT_SCHEMA) .extend(BEDJET_CLIENT_SCHEMA)
) )
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await fan.new_fan(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await fan.register_fan(var, config)
await register_bedjet_child(var, config) await register_bedjet_child(var, config)

View File

@@ -1,31 +1,28 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan, output from esphome.components import fan, output
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import CONF_DIRECTION_OUTPUT, CONF_OSCILLATION_OUTPUT, CONF_OUTPUT
CONF_DIRECTION_OUTPUT,
CONF_OSCILLATION_OUTPUT,
CONF_OUTPUT,
CONF_OUTPUT_ID,
)
from .. import binary_ns from .. import binary_ns
BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component) BinaryFan = binary_ns.class_("BinaryFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( CONFIG_SCHEMA = (
{ fan.fan_schema(BinaryFan)
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(BinaryFan), .extend(
cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput), {
cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Required(CONF_OUTPUT): cv.use_id(output.BinaryOutput),
cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput), cv.Optional(CONF_DIRECTION_OUTPUT): cv.use_id(output.BinaryOutput),
} cv.Optional(CONF_OSCILLATION_OUTPUT): cv.use_id(output.BinaryOutput),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_OUTPUT_ID]) var = await fan.new_fan(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await fan.register_fan(var, config)
output_ = await cg.get_variable(config[CONF_OUTPUT]) output_ = await cg.get_variable(config[CONF_OUTPUT])
cg.add(var.set_output(output_)) cg.add(var.set_output(output_))

View File

@@ -386,7 +386,7 @@ def validate_click_timing(value):
return value return value
BINARY_SENSOR_SCHEMA = ( _BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA) .extend(cv.MQTT_COMPONENT_SCHEMA)
.extend( .extend(
@@ -458,19 +458,17 @@ BINARY_SENSOR_SCHEMA = (
) )
) )
_UNDEF = object()
def binary_sensor_schema( def binary_sensor_schema(
class_: MockObjClass = _UNDEF, class_: MockObjClass = cv.UNDEFINED,
*, *,
icon: str = _UNDEF, icon: str = cv.UNDEFINED,
entity_category: str = _UNDEF, entity_category: str = cv.UNDEFINED,
device_class: str = _UNDEF, device_class: str = cv.UNDEFINED,
) -> cv.Schema: ) -> cv.Schema:
schema = {} schema = {}
if class_ is not _UNDEF: if class_ is not cv.UNDEFINED:
# Not cv.optional # Not cv.optional
schema[cv.GenerateID()] = cv.declare_id(class_) schema[cv.GenerateID()] = cv.declare_id(class_)
@@ -479,10 +477,15 @@ def binary_sensor_schema(
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_DEVICE_CLASS, device_class, validate_device_class),
]: ]:
if default is not _UNDEF: if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator schema[cv.Optional(key, default=default)] = validator
return BINARY_SENSOR_SCHEMA.extend(schema) return _BINARY_SENSOR_SCHEMA.extend(schema)
# Remove before 2025.11.0
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
async def setup_binary_sensor_core_(var, config): async def setup_binary_sensor_core_(var, config):

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

@@ -4,7 +4,6 @@ from esphome.components import ble_client, esp32_ble_tracker, text_sensor
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CHARACTERISTIC_UUID, CONF_CHARACTERISTIC_UUID,
CONF_ID,
CONF_NOTIFY, CONF_NOTIFY,
CONF_SERVICE_UUID, CONF_SERVICE_UUID,
CONF_TRIGGER_ID, CONF_TRIGGER_ID,
@@ -32,9 +31,9 @@ BLETextSensorNotifyTrigger = ble_client_ns.class_(
) )
CONFIG_SCHEMA = cv.All( CONFIG_SCHEMA = cv.All(
text_sensor.TEXT_SENSOR_SCHEMA.extend( text_sensor.text_sensor_schema(BLETextSensor)
.extend(
{ {
cv.GenerateID(): cv.declare_id(BLETextSensor),
cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid,
cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid,
cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid, cv.Optional(CONF_DESCRIPTOR_UUID): esp32_ble_tracker.bt_uuid,
@@ -54,7 +53,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await text_sensor.new_text_sensor(config)
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format): if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add( cg.add(
var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID])) var.set_service_uuid16(esp32_ble_tracker.as_hex(config[CONF_SERVICE_UUID]))
@@ -101,7 +100,6 @@ async def to_code(config):
await cg.register_component(var, config) await cg.register_component(var, config)
await ble_client.register_ble_node(var, config) await ble_client.register_ble_node(var, config)
cg.add(var.set_enable_notify(config[CONF_NOTIFY])) cg.add(var.set_enable_notify(config[CONF_NOTIFY]))
await text_sensor.register_text_sensor(var, config)
for conf in config.get(CONF_ON_NOTIFY, []): for conf in config.get(CONF_ON_NOTIFY, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await ble_client.register_ble_node(trigger, config) await ble_client.register_ble_node(trigger, config)

View File

@@ -73,9 +73,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->read.handle; resp.handle = param->read.handle;
resp.data.reserve(param->read.value_len); resp.data.reserve(param->read.value_len);
for (uint16_t i = 0; i < param->read.value_len; i++) { // Use bulk insert instead of individual push_backs
resp.data.push_back(param->read.value[i]); resp.data.insert(resp.data.end(), param->read.value, param->read.value + param->read.value_len);
}
this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp);
break; break;
} }
@@ -127,9 +126,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
resp.address = this->address_; resp.address = this->address_;
resp.handle = param->notify.handle; resp.handle = param->notify.handle;
resp.data.reserve(param->notify.value_len); resp.data.reserve(param->notify.value_len);
for (uint16_t i = 0; i < param->notify.value_len; i++) { // Use bulk insert instead of individual push_backs
resp.data.push_back(param->notify.value[i]); resp.data.insert(resp.data.end(), param->notify.value, param->notify.value + param->notify.value_len);
}
this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp);
break; break;
} }

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
@@ -51,33 +52,60 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
return true; return true;
} }
static constexpr size_t FLUSH_BATCH_SIZE = 8;
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
return batch_buffer;
}
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false; return false;
api::BluetoothLERawAdvertisementsResponse resp; // Get the batch buffer reference
auto &batch_buffer = get_batch_buffer();
// Reserve additional capacity if needed
size_t new_size = batch_buffer.size() + count;
if (batch_buffer.capacity() < new_size) {
batch_buffer.reserve(new_size);
}
// Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
auto &result = advertisements[i]; auto &result = advertisements[i];
api::BluetoothLERawAdvertisement adv; uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back();
auto &adv = batch_buffer.back();
adv.address = esp32_ble::ble_addr_to_uint64(result.bda); adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
adv.rssi = result.rssi; adv.rssi = result.rssi;
adv.address_type = result.ble_addr_type; adv.address_type = result.ble_addr_type;
adv.data.assign(&result.ble_adv[0], &result.ble_adv[length]);
uint8_t length = result.adv_data_len + result.scan_rsp_len; ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
adv.data.reserve(length);
for (uint16_t i = 0; i < length; i++) {
adv.data.push_back(result.ble_adv[i]);
}
resp.advertisements.push_back(std::move(adv));
ESP_LOGV(TAG, "Proxying raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi); result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
} }
ESP_LOGV(TAG, "Proxying %d packets", count);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); // Only send if we've accumulated a good batch size to maximize batching efficiency
// https://github.com/esphome/backlog/issues/21
if (batch_buffer.size() >= FLUSH_BATCH_SIZE) {
this->flush_pending_advertisements();
}
return true; return true;
} }
void BluetoothProxy::flush_pending_advertisements() {
auto &batch_buffer = get_batch_buffer();
if (batch_buffer.empty() || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
return;
api::BluetoothLERawAdvertisementsResponse resp;
resp.advertisements.swap(batch_buffer);
this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp);
}
void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) {
api::BluetoothLEAdvertisementResponse resp; api::BluetoothLEAdvertisementResponse resp;
resp.address = device.address_uint64(); resp.address = device.address_uint64();
@@ -85,21 +113,34 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi
if (!device.get_name().empty()) if (!device.get_name().empty())
resp.name = device.get_name(); resp.name = device.get_name();
resp.rssi = device.get_rssi(); resp.rssi = device.get_rssi();
for (auto uuid : device.get_service_uuids()) {
resp.service_uuids.push_back(uuid.to_string()); // Pre-allocate vectors based on known sizes
auto service_uuids = device.get_service_uuids();
resp.service_uuids.reserve(service_uuids.size());
for (auto &uuid : service_uuids) {
resp.service_uuids.emplace_back(uuid.to_string());
} }
for (auto &data : device.get_service_datas()) {
api::BluetoothServiceData service_data; // Pre-allocate service data vector
auto service_datas = device.get_service_datas();
resp.service_data.reserve(service_datas.size());
for (auto &data : service_datas) {
resp.service_data.emplace_back();
auto &service_data = resp.service_data.back();
service_data.uuid = data.uuid.to_string(); service_data.uuid = data.uuid.to_string();
service_data.data.assign(data.data.begin(), data.data.end()); service_data.data.assign(data.data.begin(), data.data.end());
resp.service_data.push_back(std::move(service_data));
} }
for (auto &data : device.get_manufacturer_datas()) {
api::BluetoothServiceData manufacturer_data; // Pre-allocate manufacturer data vector
auto manufacturer_datas = device.get_manufacturer_datas();
resp.manufacturer_data.reserve(manufacturer_datas.size());
for (auto &data : manufacturer_datas) {
resp.manufacturer_data.emplace_back();
auto &manufacturer_data = resp.manufacturer_data.back();
manufacturer_data.uuid = data.uuid.to_string(); manufacturer_data.uuid = data.uuid.to_string();
manufacturer_data.data.assign(data.data.begin(), data.data.end()); manufacturer_data.data.assign(data.data.begin(), data.data.end());
resp.manufacturer_data.push_back(std::move(manufacturer_data));
} }
this->api_connection_->send_bluetooth_le_advertisement(resp); this->api_connection_->send_bluetooth_le_advertisement(resp);
} }
@@ -133,6 +174,18 @@ void BluetoothProxy::loop() {
} }
return; return;
} }
// Flush any pending BLE advertisements that have been accumulated but not yet sent
if (this->raw_advertisements_) {
static uint32_t last_flush_time = 0;
uint32_t now = App.get_loop_component_start_time();
// Flush accumulated advertisements every 100ms
if (now - last_flush_time >= 100) {
this->flush_pending_advertisements();
last_flush_time = now;
}
}
for (auto *connection : this->connections_) { for (auto *connection : this->connections_) {
if (connection->send_service_ == connection->service_count_) { if (connection->send_service_ == connection->service_count_) {
connection->send_service_ = DONE_SENDING_SERVICES; connection->send_service_ = DONE_SENDING_SERVICES;
@@ -161,11 +214,27 @@ void BluetoothProxy::loop() {
} }
api::BluetoothGATTGetServicesResponse resp; api::BluetoothGATTGetServicesResponse resp;
resp.address = connection->get_address(); resp.address = connection->get_address();
resp.services.reserve(1); // Always one service per response in this implementation
api::BluetoothGATTService service_resp; api::BluetoothGATTService service_resp;
service_resp.uuid = get_128bit_uuid_vec(service_result.uuid); service_resp.uuid = get_128bit_uuid_vec(service_result.uuid);
service_resp.handle = service_result.start_handle; service_resp.handle = service_result.start_handle;
uint16_t char_offset = 0; uint16_t char_offset = 0;
esp_gattc_char_elem_t char_result; esp_gattc_char_elem_t char_result;
// Get the number of characteristics directly with one call
uint16_t total_char_count = 0;
esp_gatt_status_t char_count_status = esp_ble_gattc_get_attr_count(
connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_CHARACTERISTIC,
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
// Only reserve if we successfully got a count
service_resp.characteristics.reserve(total_char_count);
} else if (char_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", connection->get_connection_index(),
connection->address_str().c_str(), char_count_status);
}
// Now process characteristics
while (true) { // characteristics while (true) { // characteristics
uint16_t char_count = 1; uint16_t char_count = 1;
esp_gatt_status_t char_status = esp_ble_gattc_get_all_char( esp_gatt_status_t char_status = esp_ble_gattc_get_all_char(
@@ -187,6 +256,23 @@ void BluetoothProxy::loop() {
characteristic_resp.handle = char_result.char_handle; characteristic_resp.handle = char_result.char_handle;
characteristic_resp.properties = char_result.properties; characteristic_resp.properties = char_result.properties;
char_offset++; char_offset++;
// Get the number of descriptors directly with one call
uint16_t total_desc_count = 0;
esp_gatt_status_t desc_count_status =
esp_ble_gattc_get_attr_count(connection->get_gattc_if(), connection->get_conn_id(), ESP_GATT_DB_DESCRIPTOR,
char_result.char_handle, service_result.end_handle, 0, &total_desc_count);
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
// Only reserve if we successfully got a count
characteristic_resp.descriptors.reserve(total_desc_count);
} else if (desc_count_status != ESP_GATT_OK) {
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
connection->get_connection_index(), connection->address_str().c_str(), char_result.char_handle,
desc_count_status);
}
// Now process descriptors
uint16_t desc_offset = 0; uint16_t desc_offset = 0;
esp_gattc_descr_elem_t desc_result; esp_gattc_descr_elem_t desc_result;
while (true) { // descriptors while (true) { // descriptors

View File

@@ -56,6 +56,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
void dump_config() override; void dump_config() override;
void setup() override; void setup() override;
void loop() override; void loop() override;
void flush_pending_advertisements();
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override; esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) { void register_connection(BluetoothConnection *connection) {

View File

@@ -44,7 +44,7 @@ ButtonPressTrigger = button_ns.class_(
validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_") validate_device_class = cv.one_of(*DEVICE_CLASSES, lower=True, space="_")
BUTTON_SCHEMA = ( _BUTTON_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
@@ -60,15 +60,13 @@ BUTTON_SCHEMA = (
) )
) )
_UNDEF = object()
def button_schema( def button_schema(
class_: MockObjClass, class_: MockObjClass,
*, *,
icon: str = _UNDEF, icon: str = cv.UNDEFINED,
entity_category: str = _UNDEF, entity_category: str = cv.UNDEFINED,
device_class: str = _UNDEF, device_class: str = cv.UNDEFINED,
) -> cv.Schema: ) -> cv.Schema:
schema = {cv.GenerateID(): cv.declare_id(class_)} schema = {cv.GenerateID(): cv.declare_id(class_)}
@@ -77,10 +75,15 @@ def button_schema(
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category), (CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_DEVICE_CLASS, device_class, validate_device_class), (CONF_DEVICE_CLASS, device_class, validate_device_class),
]: ]:
if default is not _UNDEF: if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator schema[cv.Optional(key, default=default)] = validator
return BUTTON_SCHEMA.extend(schema) return _BUTTON_SCHEMA.extend(schema)
# Remove before 2025.11.0
BUTTON_SCHEMA = button_schema(Button)
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
async def setup_button_core_(var, config): async def setup_button_core_(var, config):

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

@@ -11,9 +11,11 @@ from esphome.const import (
CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CURRENT_TEMPERATURE_STATE_TOPIC,
CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_FAN_MODE,
CONF_CUSTOM_PRESET, CONF_CUSTOM_PRESET,
CONF_ENTITY_CATEGORY,
CONF_FAN_MODE, CONF_FAN_MODE,
CONF_FAN_MODE_COMMAND_TOPIC, CONF_FAN_MODE_COMMAND_TOPIC,
CONF_FAN_MODE_STATE_TOPIC, CONF_FAN_MODE_STATE_TOPIC,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MAX_TEMPERATURE, CONF_MAX_TEMPERATURE,
CONF_MIN_TEMPERATURE, CONF_MIN_TEMPERATURE,
@@ -46,6 +48,7 @@ from esphome.const import (
CONF_WEB_SERVER, CONF_WEB_SERVER,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -151,12 +154,11 @@ ControlTrigger = climate_ns.class_(
"ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref")) "ControlTrigger", automation.Trigger.template(ClimateCall.operator("ref"))
) )
CLIMATE_SCHEMA = ( _CLIMATE_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(Climate),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTClimateComponent),
cv.Optional(CONF_VISUAL, default={}): cv.Schema( cv.Optional(CONF_VISUAL, default={}): cv.Schema(
{ {
@@ -245,6 +247,31 @@ CLIMATE_SCHEMA = (
) )
def climate_schema(
class_: MockObjClass,
*,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _CLIMATE_SCHEMA.extend(schema)
# Remove before 2025.11.0
CLIMATE_SCHEMA = climate_schema(Climate)
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
async def setup_climate_core_(var, config): async def setup_climate_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
@@ -419,6 +446,12 @@ async def register_climate(var, config):
await setup_climate_core_(var, config) await setup_climate_core_(var, config)
async def new_climate(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_climate(var, config)
return var
CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema( CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
{ {
cv.Required(CONF_ID): cv.use_id(Climate), cv.Required(CONF_ID): cv.use_id(Climate),

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

@@ -5,7 +5,6 @@ from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY, CONF_ENTITY_CATEGORY,
CONF_ICON, CONF_ICON,
CONF_ID,
CONF_SOURCE_ID, CONF_SOURCE_ID,
) )
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
@@ -15,12 +14,15 @@ from .. import copy_ns
CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component) CopyCover = copy_ns.class_("CopyCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
{ cover.cover_schema(CopyCover)
cv.GenerateID(): cv.declare_id(CopyCover), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(cover.Cover),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
await cover.register_cover(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import fan from esphome.components import fan
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
from .. import copy_ns from .. import copy_ns
@@ -9,12 +9,15 @@ from .. import copy_ns
CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component) CopyFan = copy_ns.class_("CopyFan", fan.Fan, cg.Component)
CONFIG_SCHEMA = fan.FAN_SCHEMA.extend( CONFIG_SCHEMA = (
{ fan.fan_schema(CopyFan)
cv.GenerateID(): cv.declare_id(CopyFan), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(fan.Fan),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await fan.new_fan(config)
await fan.register_fan(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg import esphome.codegen as cg
from esphome.components import lock from esphome.components import lock
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_ID, CONF_SOURCE_ID from esphome.const import CONF_ENTITY_CATEGORY, CONF_ICON, CONF_SOURCE_ID
from esphome.core.entity_helpers import inherit_property_from from esphome.core.entity_helpers import inherit_property_from
from .. import copy_ns from .. import copy_ns
@@ -9,12 +9,15 @@ from .. import copy_ns
CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component) CopyLock = copy_ns.class_("CopyLock", lock.Lock, cg.Component)
CONFIG_SCHEMA = lock.LOCK_SCHEMA.extend( CONFIG_SCHEMA = (
{ lock.lock_schema(CopyLock)
cv.GenerateID(): cv.declare_id(CopyLock), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(lock.Lock),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),
@@ -23,8 +26,7 @@ FINAL_VALIDATE_SCHEMA = cv.All(
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await lock.new_lock(config)
await lock.register_lock(var, config)
await cg.register_component(var, config) await cg.register_component(var, config)
source = await cg.get_variable(config[CONF_SOURCE_ID]) source = await cg.get_variable(config[CONF_SOURCE_ID])

View File

@@ -9,12 +9,15 @@ from .. import copy_ns
CopyText = copy_ns.class_("CopyText", text.Text, cg.Component) CopyText = copy_ns.class_("CopyText", text.Text, cg.Component)
CONFIG_SCHEMA = text.TEXT_SCHEMA.extend( CONFIG_SCHEMA = (
{ text.text_schema(CopyText)
cv.GenerateID(): cv.declare_id(CopyText), .extend(
cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text), {
} cv.Required(CONF_SOURCE_ID): cv.use_id(text.Text),
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = cv.All( FINAL_VALIDATE_SCHEMA = cv.All(
inherit_property_from(CONF_ICON, CONF_SOURCE_ID), inherit_property_from(CONF_ICON, CONF_SOURCE_ID),

View File

@@ -5,6 +5,8 @@ from esphome.components import mqtt, web_server
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ICON,
CONF_ID, CONF_ID,
CONF_MQTT_ID, CONF_MQTT_ID,
CONF_ON_OPEN, CONF_ON_OPEN,
@@ -31,6 +33,7 @@ from esphome.const import (
DEVICE_CLASS_WINDOW, DEVICE_CLASS_WINDOW,
) )
from esphome.core import CORE, coroutine_with_priority from esphome.core import CORE, coroutine_with_priority
from esphome.cpp_generator import MockObjClass
from esphome.cpp_helpers import setup_entity from esphome.cpp_helpers import setup_entity
IS_PLATFORM_COMPONENT = True IS_PLATFORM_COMPONENT = True
@@ -89,12 +92,11 @@ CoverClosedTrigger = cover_ns.class_(
CONF_ON_CLOSED = "on_closed" CONF_ON_CLOSED = "on_closed"
COVER_SCHEMA = ( _COVER_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA) cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA) .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
.extend( .extend(
{ {
cv.GenerateID(): cv.declare_id(Cover),
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent), cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTCoverComponent),
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True), cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All( cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
@@ -124,6 +126,33 @@ COVER_SCHEMA = (
) )
def cover_schema(
class_: MockObjClass,
*,
device_class: str = cv.UNDEFINED,
entity_category: str = cv.UNDEFINED,
icon: str = cv.UNDEFINED,
) -> cv.Schema:
schema = {
cv.GenerateID(): cv.declare_id(class_),
}
for key, default, validator in [
(CONF_DEVICE_CLASS, device_class, cv.one_of(*DEVICE_CLASSES, lower=True)),
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
(CONF_ICON, icon, cv.icon),
]:
if default is not cv.UNDEFINED:
schema[cv.Optional(key, default=default)] = validator
return _COVER_SCHEMA.extend(schema)
# Remove before 2025.11.0
COVER_SCHEMA = cover_schema(Cover)
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
async def setup_cover_core_(var, config): async def setup_cover_core_(var, config):
await setup_entity(var, config) await setup_entity(var, config)
@@ -163,6 +192,12 @@ async def register_cover(var, config):
await setup_cover_core_(var, config) await setup_cover_core_(var, config)
async def new_cover(config, *args):
var = cg.new_Pvariable(config[CONF_ID], *args)
await register_cover(var, config)
return var
COVER_ACTION_SCHEMA = maybe_simple_id( COVER_ACTION_SCHEMA = maybe_simple_id(
{ {
cv.Required(CONF_ID): cv.use_id(Cover), cv.Required(CONF_ID): cv.use_id(Cover),

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

@@ -5,7 +5,6 @@ import esphome.config_validation as cv
from esphome.const import ( from esphome.const import (
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION, CONF_CLOSE_DURATION,
CONF_ID,
CONF_MAX_DURATION, CONF_MAX_DURATION,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
CONF_OPEN_DURATION, CONF_OPEN_DURATION,
@@ -30,45 +29,47 @@ CurrentBasedCover = current_based_ns.class_(
"CurrentBasedCover", cover.Cover, cg.Component "CurrentBasedCover", cover.Cover, cg.Component
) )
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
{ cover.cover_schema(CurrentBasedCover)
cv.GenerateID(): cv.declare_id(CurrentBasedCover), .extend(
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), {
cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range( cv.Required(CONF_OPEN_SENSOR): cv.use_id(sensor.Sensor),
min=0, min_included=False cv.Required(CONF_OPEN_MOVING_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( ),
min=0, min_included=False cv.Optional(CONF_OPEN_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), ),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor), cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range( cv.Required(CONF_CLOSE_SENSOR): cv.use_id(sensor.Sensor),
min=0, min_included=False cv.Required(CONF_CLOSE_MOVING_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range( ),
min=0, min_included=False cv.Optional(CONF_CLOSE_OBSTACLE_CURRENT_THRESHOLD): cv.float_range(
), min=0, min_included=False
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), ),
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage,
cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean, cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation( cv.Optional(CONF_MALFUNCTION_DETECTION, default=True): cv.boolean,
single=True cv.Optional(CONF_MALFUNCTION_ACTION): automation.validate_automation(
), single=True
cv.Optional( ),
CONF_START_SENSING_DELAY, default="500ms" cv.Optional(
): cv.positive_time_period_milliseconds, CONF_START_SENSING_DELAY, default="500ms"
} ): cv.positive_time_period_milliseconds,
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await cover.register_cover(var, config)
await automation.build_automation( await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION] var.get_stop_trigger(), [], config[CONF_STOP_ACTION]

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

@@ -56,21 +56,13 @@ void DallasTemperatureSensor::update() {
}); });
} }
void IRAM_ATTR DallasTemperatureSensor::read_scratch_pad_int_() {
for (uint8_t &i : this->scratch_pad_) {
i = this->bus_->read8();
}
}
bool DallasTemperatureSensor::read_scratch_pad_() { bool DallasTemperatureSensor::read_scratch_pad_() {
bool success; bool success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD);
{ if (success) {
InterruptLock lock; for (uint8_t &i : this->scratch_pad_) {
success = this->send_command_(DALLAS_COMMAND_READ_SCRATCH_PAD); i = this->bus_->read8();
if (success) }
this->read_scratch_pad_int_(); } else {
}
if (!success) {
ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str()); ESP_LOGW(TAG, "'%s' - reading scratch pad failed bus reset", this->get_name().c_str());
this->status_set_warning("bus reset failed"); this->status_set_warning("bus reset failed");
} }
@@ -113,17 +105,14 @@ void DallasTemperatureSensor::setup() {
return; return;
this->scratch_pad_[4] = res; this->scratch_pad_[4] = res;
{ if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) {
InterruptLock lock; this->bus_->write8(this->scratch_pad_[2]); // high alarm temp
if (this->send_command_(DALLAS_COMMAND_WRITE_SCRATCH_PAD)) { this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
this->bus_->write8(this->scratch_pad_[2]); // high alarm temp this->bus_->write8(this->scratch_pad_[4]); // resolution
this->bus_->write8(this->scratch_pad_[3]); // low alarm temp
this->bus_->write8(this->scratch_pad_[4]); // resolution
}
// write value to EEPROM
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
} }
// write value to EEPROM
this->send_command_(DALLAS_COMMAND_COPY_SCRATCH_PAD);
} }
bool DallasTemperatureSensor::check_scratch_pad_() { bool DallasTemperatureSensor::check_scratch_pad_() {
@@ -138,6 +127,10 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
if (!chksum_validity) { if (!chksum_validity) {
ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str());
this->status_set_warning("scratch pad checksum invalid"); this->status_set_warning("scratch pad checksum invalid");
ESP_LOGD(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0],
this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4],
this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8],
crc8(this->scratch_pad_, 8));
} }
return chksum_validity; return chksum_validity;
} }

View File

@@ -23,7 +23,6 @@ class DallasTemperatureSensor : public PollingComponent, public sensor::Sensor,
/// Get the number of milliseconds we have to wait for the conversion phase. /// Get the number of milliseconds we have to wait for the conversion phase.
uint16_t millis_to_wait_for_conversion_() const; uint16_t millis_to_wait_for_conversion_() const;
bool read_scratch_pad_(); bool read_scratch_pad_();
void read_scratch_pad_int_();
bool check_scratch_pad_(); bool check_scratch_pad_();
float get_temp_c_(); float get_temp_c_();
}; };

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

@@ -17,7 +17,6 @@ from esphome.const import (
CONF_DEVICE_CLASS, CONF_DEVICE_CLASS,
CONF_FORCE_UPDATE, CONF_FORCE_UPDATE,
CONF_ICON, CONF_ICON,
CONF_ID,
CONF_INVERTED, CONF_INVERTED,
CONF_MAX_VALUE, CONF_MAX_VALUE,
CONF_MIN_VALUE, CONF_MIN_VALUE,
@@ -153,9 +152,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
climate.CLIMATE_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( climate.climate_schema(DemoClimate)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(DemoClimate),
cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(CLIMATE_TYPES, int=True),
} }
) )
@@ -183,9 +183,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
cover.COVER_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( cover.cover_schema(DemoCover)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(): cv.declare_id(DemoCover),
cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(COVER_TYPES, int=True),
} }
) )
@@ -211,9 +212,10 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
fan.FAN_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( fan.fan_schema(DemoFan)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoFan),
cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(FAN_TYPES, int=True),
} }
) )
@@ -251,7 +253,9 @@ CONFIG_SCHEMA = cv.Schema(
}, },
], ],
): [ ): [
light.RGB_LIGHT_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( light.light_schema(DemoLight, light.LightType.RGB)
.extend(cv.COMPONENT_SCHEMA)
.extend(
{ {
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight), cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(DemoLight),
cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True), cv.Required(CONF_TYPE): cv.enum(LIGHT_TYPES, int=True),
@@ -377,39 +381,33 @@ async def to_code(config):
await cg.register_component(var, conf) await cg.register_component(var, conf)
for conf in config[CONF_CLIMATES]: for conf in config[CONF_CLIMATES]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await climate.new_climate(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await climate.register_climate(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_COVERS]: for conf in config[CONF_COVERS]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await cover.new_cover(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await cover.register_cover(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_FANS]: for conf in config[CONF_FANS]:
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) var = await fan.new_fan(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await fan.register_fan(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_LIGHTS]: for conf in config[CONF_LIGHTS]:
var = cg.new_Pvariable(conf[CONF_OUTPUT_ID]) var = await light.new_light(conf)
await cg.register_component(var, conf) await cg.register_component(var, conf)
await light.register_light(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_NUMBERS]: for conf in config[CONF_NUMBERS]:
var = cg.new_Pvariable(conf[CONF_ID]) var = await number.new_number(
await cg.register_component(var, conf)
await number.register_number(
var,
conf, conf,
min_value=conf[CONF_MIN_VALUE], min_value=conf[CONF_MIN_VALUE],
max_value=conf[CONF_MAX_VALUE], max_value=conf[CONF_MAX_VALUE],
step=conf[CONF_STEP], step=conf[CONF_STEP],
) )
await cg.register_component(var, conf)
cg.add(var.set_type(conf[CONF_TYPE])) cg.add(var.set_type(conf[CONF_TYPE]))
for conf in config[CONF_SENSORS]: for conf in config[CONF_SENSORS]:

View File

@@ -2,6 +2,7 @@ import esphome.codegen as cg
from esphome.components import switch from esphome.components import switch
import esphome.config_validation as cv import esphome.config_validation as cv
from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG from esphome.const import CONF_TYPE, ENTITY_CATEGORY_CONFIG
from esphome.cpp_generator import MockObjClass
from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component from .. import CONF_DFROBOT_SEN0395_ID, DfrobotSen0395Component
@@ -26,32 +27,30 @@ Sen0395StartAfterBootSwitch = dfrobot_sen0395_ns.class_(
"Sen0395StartAfterBootSwitch", DfrobotSen0395Switch "Sen0395StartAfterBootSwitch", DfrobotSen0395Switch
) )
_SWITCH_SCHEMA = (
switch.switch_schema( def _switch_schema(class_: MockObjClass) -> cv.Schema:
entity_category=ENTITY_CATEGORY_CONFIG, return (
switch.switch_schema(
class_,
entity_category=ENTITY_CATEGORY_CONFIG,
)
.extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(
DfrobotSen0395Component
),
}
)
.extend(cv.COMPONENT_SCHEMA)
) )
.extend(
{
cv.GenerateID(CONF_DFROBOT_SEN0395_ID): cv.use_id(DfrobotSen0395Component),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
CONFIG_SCHEMA = cv.typed_schema( CONFIG_SCHEMA = cv.typed_schema(
{ {
"sensor_active": _SWITCH_SCHEMA.extend( "sensor_active": _switch_schema(Sen0395PowerSwitch),
{cv.GenerateID(): cv.declare_id(Sen0395PowerSwitch)} "turn_on_led": _switch_schema(Sen0395LedSwitch),
), "presence_via_uart": _switch_schema(Sen0395UartPresenceSwitch),
"turn_on_led": _SWITCH_SCHEMA.extend( "start_after_boot": _switch_schema(Sen0395StartAfterBootSwitch),
{cv.GenerateID(): cv.declare_id(Sen0395LedSwitch)}
),
"presence_via_uart": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395UartPresenceSwitch)}
),
"start_after_boot": _SWITCH_SCHEMA.extend(
{cv.GenerateID(): cv.declare_id(Sen0395StartAfterBootSwitch)}
),
} }
) )

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

@@ -6,7 +6,6 @@ from esphome.const import (
CONF_CLOSE_ACTION, CONF_CLOSE_ACTION,
CONF_CLOSE_DURATION, CONF_CLOSE_DURATION,
CONF_CLOSE_ENDSTOP, CONF_CLOSE_ENDSTOP,
CONF_ID,
CONF_MAX_DURATION, CONF_MAX_DURATION,
CONF_OPEN_ACTION, CONF_OPEN_ACTION,
CONF_OPEN_DURATION, CONF_OPEN_DURATION,
@@ -17,25 +16,27 @@ from esphome.const import (
endstop_ns = cg.esphome_ns.namespace("endstop") endstop_ns = cg.esphome_ns.namespace("endstop")
EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component) EndstopCover = endstop_ns.class_("EndstopCover", cover.Cover, cg.Component)
CONFIG_SCHEMA = cover.COVER_SCHEMA.extend( CONFIG_SCHEMA = (
{ cover.cover_schema(EndstopCover)
cv.GenerateID(): cv.declare_id(EndstopCover), .extend(
cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), {
cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), cv.Required(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds,
cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True),
cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor),
cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds,
} cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds,
).extend(cv.COMPONENT_SCHEMA) }
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config): async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID]) var = await cover.new_cover(config)
await cg.register_component(var, config) await cg.register_component(var, config)
await cover.register_cover(var, config)
await automation.build_automation( await automation.build_automation(
var.get_stop_trigger(), [], config[CONF_STOP_ACTION] var.get_stop_trigger(), [], config[CONF_STOP_ACTION]

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
@@ -60,6 +59,7 @@ from .const import ( # noqa
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
VARIANT_FRIENDLY, VARIANT_FRIENDLY,
@@ -90,6 +90,7 @@ CPU_FREQUENCIES = {
VARIANT_ESP32C3: get_cpu_frequencies(80, 160), VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
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 +190,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 +207,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 +297,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 +370,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

@@ -4,6 +4,7 @@ from .const import (
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2, VARIANT_ESP32S2,
VARIANT_ESP32S3, VARIANT_ESP32S3,
) )
@@ -1632,6 +1633,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

@@ -19,6 +19,7 @@ VARIANT_ESP32C2 = "ESP32C2"
VARIANT_ESP32C3 = "ESP32C3" VARIANT_ESP32C3 = "ESP32C3"
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,
@@ -27,6 +28,7 @@ VARIANTS = [
VARIANT_ESP32C3, VARIANT_ESP32C3,
VARIANT_ESP32C6, VARIANT_ESP32C6,
VARIANT_ESP32H2, VARIANT_ESP32H2,
VARIANT_ESP32P4,
] ]
VARIANT_FRIENDLY = { VARIANT_FRIENDLY = {
@@ -37,6 +39,7 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32C3: "ESP32-C3", VARIANT_ESP32C3: "ESP32-C3",
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

@@ -2,42 +2,66 @@
#include "gpio.h" #include "gpio.h"
#include "esphome/core/log.h" #include "esphome/core/log.h"
#include "driver/gpio.h"
#include "driver/rtc_io.h"
#include "hal/gpio_hal.h"
#include "soc/soc_caps.h"
#include "soc/gpio_periph.h"
#include <cinttypes> #include <cinttypes>
#if (SOC_RTCIO_PIN_COUNT > 0)
#include "hal/rtc_io_hal.h"
#endif
#ifndef SOC_GPIO_SUPPORT_RTC_INDEPENDENT
#define SOC_GPIO_SUPPORT_RTC_INDEPENDENT 0 // NOLINT
#endif
namespace esphome { namespace esphome {
namespace esp32 { namespace esp32 {
static const char *const TAG = "esp32"; static const char *const TAG = "esp32";
static const gpio_hal_context_t GPIO_HAL = {.dev = GPIO_HAL_GET_HW(GPIO_PORT_0)};
bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) bool ESP32InternalGPIOPin::isr_service_installed = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static gpio_mode_t IRAM_ATTR flags_to_mode(gpio::Flags flags) { static gpio_mode_t flags_to_mode(gpio::Flags flags) {
flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN)); flags = (gpio::Flags)(flags & ~(gpio::FLAG_PULLUP | gpio::FLAG_PULLDOWN));
if (flags == gpio::FLAG_INPUT) { if (flags == gpio::FLAG_INPUT)
return GPIO_MODE_INPUT; return GPIO_MODE_INPUT;
} else if (flags == gpio::FLAG_OUTPUT) { if (flags == gpio::FLAG_OUTPUT)
return GPIO_MODE_OUTPUT; return GPIO_MODE_OUTPUT;
} else if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { if (flags == (gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_OUTPUT_OD; return GPIO_MODE_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN)) { if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT | gpio::FLAG_OPEN_DRAIN))
return GPIO_MODE_INPUT_OUTPUT_OD; return GPIO_MODE_INPUT_OUTPUT_OD;
} else if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT)) { if (flags == (gpio::FLAG_INPUT | gpio::FLAG_OUTPUT))
return GPIO_MODE_INPUT_OUTPUT; return GPIO_MODE_INPUT_OUTPUT;
} else { // unsupported or gpio::FLAG_NONE
// unsupported or gpio::FLAG_NONE return GPIO_MODE_DISABLE;
return GPIO_MODE_DISABLE;
}
} }
struct ISRPinArg { struct ISRPinArg {
gpio_num_t pin; gpio_num_t pin;
gpio::Flags flags;
bool inverted; bool inverted;
#if defined(USE_ESP32_VARIANT_ESP32)
bool use_rtc;
int rtc_pin;
#endif
}; };
ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const { ISRInternalGPIOPin ESP32InternalGPIOPin::to_isr() const {
auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory) auto *arg = new ISRPinArg{}; // NOLINT(cppcoreguidelines-owning-memory)
arg->pin = pin_; arg->pin = this->pin_;
arg->flags = gpio::FLAG_NONE;
arg->inverted = inverted_; arg->inverted = inverted_;
#if defined(USE_ESP32_VARIANT_ESP32)
arg->use_rtc = rtc_gpio_is_valid_gpio(this->pin_);
if (arg->use_rtc)
arg->rtc_pin = rtc_io_number_get(this->pin_);
#endif
return ISRInternalGPIOPin((void *) arg); return ISRInternalGPIOPin((void *) arg);
} }
@@ -90,6 +114,7 @@ void ESP32InternalGPIOPin::setup() {
if (flags_ & gpio::FLAG_OUTPUT) { if (flags_ & gpio::FLAG_OUTPUT) {
gpio_set_drive_capability(pin_, drive_strength_); gpio_set_drive_capability(pin_, drive_strength_);
} }
ESP_LOGD(TAG, "rtc: %d", SOC_GPIO_SUPPORT_RTC_INDEPENDENT);
} }
void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) { void ESP32InternalGPIOPin::pin_mode(gpio::Flags flags) {
@@ -115,28 +140,65 @@ void ESP32InternalGPIOPin::detach_interrupt() const { gpio_intr_disable(pin_); }
using namespace esp32; using namespace esp32;
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() { bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
return bool(gpio_get_level(arg->pin)) != arg->inverted; return bool(gpio_hal_get_level(&GPIO_HAL, arg->pin)) != arg->inverted;
} }
void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) { void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(this->arg_);
gpio_set_level(arg->pin, value != arg->inverted ? 1 : 0); gpio_hal_set_level(&GPIO_HAL, arg->pin, value != arg->inverted);
} }
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() { void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
// not supported // not supported
} }
void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) { void IRAM_ATTR ISRInternalGPIOPin::pin_mode(gpio::Flags flags) {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_); auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
gpio_set_direction(arg->pin, flags_to_mode(flags)); gpio::Flags diff = (gpio::Flags)(flags ^ arg->flags);
gpio_pull_mode_t pull_mode = GPIO_FLOATING; if (diff & gpio::FLAG_OUTPUT) {
if ((flags & gpio::FLAG_PULLUP) && (flags & gpio::FLAG_PULLDOWN)) { if (flags & gpio::FLAG_OUTPUT) {
pull_mode = GPIO_PULLUP_PULLDOWN; gpio_hal_output_enable(&GPIO_HAL, arg->pin);
} else if (flags & gpio::FLAG_PULLUP) { if (flags & gpio::FLAG_OPEN_DRAIN)
pull_mode = GPIO_PULLUP_ONLY; gpio_hal_od_enable(&GPIO_HAL, arg->pin);
} else if (flags & gpio::FLAG_PULLDOWN) { } else {
pull_mode = GPIO_PULLDOWN_ONLY; gpio_hal_output_disable(&GPIO_HAL, arg->pin);
}
} }
gpio_set_pull_mode(arg->pin, pull_mode); if (diff & gpio::FLAG_INPUT) {
if (flags & gpio::FLAG_INPUT) {
gpio_hal_input_enable(&GPIO_HAL, arg->pin);
#if defined(USE_ESP32_VARIANT_ESP32)
if (arg->use_rtc) {
if (flags & gpio::FLAG_PULLUP) {
rtcio_hal_pullup_enable(arg->rtc_pin);
} else {
rtcio_hal_pullup_disable(arg->rtc_pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
rtcio_hal_pulldown_enable(arg->rtc_pin);
} else {
rtcio_hal_pulldown_disable(arg->rtc_pin);
}
} else
#endif
{
if (flags & gpio::FLAG_PULLUP) {
gpio_hal_pullup_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pullup_dis(&GPIO_HAL, arg->pin);
}
if (flags & gpio::FLAG_PULLDOWN) {
gpio_hal_pulldown_en(&GPIO_HAL, arg->pin);
} else {
gpio_hal_pulldown_dis(&GPIO_HAL, arg->pin);
}
}
} else {
gpio_hal_input_disable(&GPIO_HAL, arg->pin);
}
}
arg->flags = flags;
} }
} // namespace esphome } // namespace esphome

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