mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 16:41:50 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			bluetooth_
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					0475dd8af4 | 
@@ -1,4 +1,2 @@
 | 
			
		||||
[run]
 | 
			
		||||
omit = 
 | 
			
		||||
    esphome/components/*
 | 
			
		||||
    tests/integration/*
 | 
			
		||||
omit = esphome/components/*
 | 
			
		||||
 
 | 
			
		||||
@@ -1,37 +0,0 @@
 | 
			
		||||
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
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ESPHome Dev",
 | 
			
		||||
  "context": "..",
 | 
			
		||||
  "dockerFile": "Dockerfile",
 | 
			
		||||
  "image": "ghcr.io/esphome/esphome-lint:dev",
 | 
			
		||||
  "postCreateCommand": [
 | 
			
		||||
    "script/devcontainer-post-create"
 | 
			
		||||
  ],
 | 
			
		||||
  "features": {
 | 
			
		||||
    "ghcr.io/devcontainers/features/github-cli:1": {}
 | 
			
		||||
  "containerEnv": {
 | 
			
		||||
    "DEVCONTAINER": "1",
 | 
			
		||||
    "PIP_BREAK_SYSTEM_PACKAGES": "1",
 | 
			
		||||
    "PIP_ROOT_USER_ACTION": "ignore"
 | 
			
		||||
  },
 | 
			
		||||
  "runArgs": [
 | 
			
		||||
    "--privileged",
 | 
			
		||||
    "-e",
 | 
			
		||||
    "GIT_EDITOR=code --wait"
 | 
			
		||||
    "ESPHOME_DASHBOARD_USE_PING=1"
 | 
			
		||||
    // uncomment and edit the path in order to pass though local USB serial to the conatiner
 | 
			
		||||
    // , "--device=/dev/ttyACM0"
 | 
			
		||||
  ],
 | 
			
		||||
@@ -30,7 +31,7 @@
 | 
			
		||||
        "ms-python.python",
 | 
			
		||||
        "ms-python.pylint",
 | 
			
		||||
        "ms-python.flake8",
 | 
			
		||||
        "charliermarsh.ruff",
 | 
			
		||||
        "ms-python.black-formatter",
 | 
			
		||||
        "visualstudioexptteam.vscodeintellicode",
 | 
			
		||||
        // yaml
 | 
			
		||||
        "redhat.vscode-yaml",
 | 
			
		||||
@@ -48,11 +49,14 @@
 | 
			
		||||
        "flake8.args": [
 | 
			
		||||
          "--config=${workspaceFolder}/.flake8"
 | 
			
		||||
        ],
 | 
			
		||||
        "ruff.configuration": "${workspaceFolder}/pyproject.toml",
 | 
			
		||||
        "black-formatter.args": [
 | 
			
		||||
          "--config",
 | 
			
		||||
          "${workspaceFolder}/pyproject.toml"
 | 
			
		||||
        ],
 | 
			
		||||
        "[python]": {
 | 
			
		||||
          // VS will say "Value is not accepted" before building the devcontainer, but the warning
 | 
			
		||||
          // should go away after build is completed.
 | 
			
		||||
          "editor.defaultFormatter": "charliermarsh.ruff"
 | 
			
		||||
          "editor.defaultFormatter": "ms-python.black-formatter"
 | 
			
		||||
        },
 | 
			
		||||
        "editor.formatOnPaste": false,
 | 
			
		||||
        "editor.formatOnSave": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -114,5 +114,4 @@ config/
 | 
			
		||||
examples/
 | 
			
		||||
Dockerfile
 | 
			
		||||
.git/
 | 
			
		||||
tests/
 | 
			
		||||
.*
 | 
			
		||||
tests/build/
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,4 @@
 | 
			
		||||
---
 | 
			
		||||
# These are supported funding model platforms
 | 
			
		||||
 | 
			
		||||
custom: https://www.nabucasa.com
 | 
			
		||||
							
								
								
									
										33
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.github/actions/build-image/action.yaml
									
									
									
									
										vendored
									
									
								
							@@ -1,11 +1,15 @@
 | 
			
		||||
name: Build Image
 | 
			
		||||
inputs:
 | 
			
		||||
  platform:
 | 
			
		||||
    description: "Platform to build for"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "linux/amd64"
 | 
			
		||||
  target:
 | 
			
		||||
    description: "Target to build"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "docker"
 | 
			
		||||
  build_type:
 | 
			
		||||
    description: "Build type"
 | 
			
		||||
  baseimg:
 | 
			
		||||
    description: "Base image type"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "docker"
 | 
			
		||||
  suffix:
 | 
			
		||||
@@ -15,11 +19,6 @@ inputs:
 | 
			
		||||
    description: "Version to build"
 | 
			
		||||
    required: true
 | 
			
		||||
    example: "2023.12.0"
 | 
			
		||||
  base_os:
 | 
			
		||||
    description: "Base OS to use"
 | 
			
		||||
    required: false
 | 
			
		||||
    default: "debian"
 | 
			
		||||
    example: "debian"
 | 
			
		||||
runs:
 | 
			
		||||
  using: "composite"
 | 
			
		||||
  steps:
 | 
			
		||||
@@ -47,52 +46,52 @@ runs:
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to ghcr by digest
 | 
			
		||||
      id: build-ghcr
 | 
			
		||||
      uses: docker/build-push-action@v6.18.0
 | 
			
		||||
      uses: docker/build-push-action@v6.10.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
      with:
 | 
			
		||||
        context: .
 | 
			
		||||
        file: ./docker/Dockerfile
 | 
			
		||||
        platforms: ${{ inputs.platform }}
 | 
			
		||||
        target: ${{ inputs.target }}
 | 
			
		||||
        cache-from: type=gha
 | 
			
		||||
        cache-to: ${{ steps.cache-to.outputs.value }}
 | 
			
		||||
        build-args: |
 | 
			
		||||
          BUILD_TYPE=${{ inputs.build_type }}
 | 
			
		||||
          BASEIMGTYPE=${{ inputs.baseimg }}
 | 
			
		||||
          BUILD_VERSION=${{ inputs.version }}
 | 
			
		||||
          BUILD_OS=${{ inputs.base_os }}
 | 
			
		||||
        outputs: |
 | 
			
		||||
          type=image,name=ghcr.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
 | 
			
		||||
 | 
			
		||||
    - name: Export ghcr digests
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.build_type }}/ghcr
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.target }}/ghcr
 | 
			
		||||
        digest="${{ steps.build-ghcr.outputs.digest }}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.build_type }}/ghcr/${digest#sha256:}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.target }}/ghcr/${digest#sha256:}"
 | 
			
		||||
 | 
			
		||||
    - name: Build and push to dockerhub by digest
 | 
			
		||||
      id: build-dockerhub
 | 
			
		||||
      uses: docker/build-push-action@v6.18.0
 | 
			
		||||
      uses: docker/build-push-action@v6.10.0
 | 
			
		||||
      env:
 | 
			
		||||
        DOCKER_BUILD_SUMMARY: false
 | 
			
		||||
        DOCKER_BUILD_RECORD_UPLOAD: false
 | 
			
		||||
      with:
 | 
			
		||||
        context: .
 | 
			
		||||
        file: ./docker/Dockerfile
 | 
			
		||||
        platforms: ${{ inputs.platform }}
 | 
			
		||||
        target: ${{ inputs.target }}
 | 
			
		||||
        cache-from: type=gha
 | 
			
		||||
        cache-to: ${{ steps.cache-to.outputs.value }}
 | 
			
		||||
        build-args: |
 | 
			
		||||
          BUILD_TYPE=${{ inputs.build_type }}
 | 
			
		||||
          BASEIMGTYPE=${{ inputs.baseimg }}
 | 
			
		||||
          BUILD_VERSION=${{ inputs.version }}
 | 
			
		||||
          BUILD_OS=${{ inputs.base_os }}
 | 
			
		||||
        outputs: |
 | 
			
		||||
          type=image,name=docker.io/${{ steps.tags.outputs.image_name }},push-by-digest=true,name-canonical=true,push=true
 | 
			
		||||
 | 
			
		||||
    - name: Export dockerhub digests
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: |
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.build_type }}/dockerhub
 | 
			
		||||
        mkdir -p /tmp/digests/${{ inputs.target }}/dockerhub
 | 
			
		||||
        digest="${{ steps.build-dockerhub.outputs.digest }}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.build_type }}/dockerhub/${digest#sha256:}"
 | 
			
		||||
        touch "/tmp/digests/${{ inputs.target }}/dockerhub/${digest#sha256:}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,12 +17,12 @@ runs:
 | 
			
		||||
  steps:
 | 
			
		||||
    - name: Set up Python ${{ inputs.python-version }}
 | 
			
		||||
      id: python
 | 
			
		||||
      uses: actions/setup-python@v5.6.0
 | 
			
		||||
      uses: actions/setup-python@v5.3.0
 | 
			
		||||
      with:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - name: Restore Python virtual environment
 | 
			
		||||
      id: cache-venv
 | 
			
		||||
      uses: actions/cache/restore@v4.2.3
 | 
			
		||||
      uses: actions/cache/restore@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        path: venv
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -34,7 +34,7 @@ runs:
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        source venv/bin/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
    - name: Create Python virtual environment
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
 | 
			
		||||
@@ -43,5 +43,5 @@ runs:
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        ./venv/Scripts/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,6 +17,7 @@ updates:
 | 
			
		||||
      docker-actions:
 | 
			
		||||
        applies-to: version-updates
 | 
			
		||||
        patterns:
 | 
			
		||||
          - "docker/setup-qemu-action"
 | 
			
		||||
          - "docker/login-action"
 | 
			
		||||
          - "docker/setup-buildx-action"
 | 
			
		||||
  - package-ecosystem: github-actions
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,9 +21,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
 | 
			
		||||
@@ -57,17 +57,6 @@ jobs:
 | 
			
		||||
              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.'
 | 
			
		||||
            })
 | 
			
		||||
      - 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()
 | 
			
		||||
        name: Dismiss review
 | 
			
		||||
        uses: actions/github-script@v7.0.1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -33,23 +33,22 @@ concurrency:
 | 
			
		||||
jobs:
 | 
			
		||||
  check-docker:
 | 
			
		||||
    name: Build docker containers
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: ["ubuntu-24.04", "ubuntu-24.04-arm"]
 | 
			
		||||
        build_type:
 | 
			
		||||
          - "ha-addon"
 | 
			
		||||
          - "docker"
 | 
			
		||||
          # - "lint"
 | 
			
		||||
        arch: [amd64, armv7, aarch64]
 | 
			
		||||
        build_type: ["ha-addon", "docker", "lint"]
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.10"
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.7.1
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Set TAG
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -59,6 +58,6 @@ jobs:
 | 
			
		||||
        run: |
 | 
			
		||||
          docker/build.py \
 | 
			
		||||
            --tag "${TAG}" \
 | 
			
		||||
            --arch "${{ matrix.os == 'ubuntu-24.04-arm' && 'aarch64' || 'amd64' }}" \
 | 
			
		||||
            --arch "${{ matrix.arch }}" \
 | 
			
		||||
            --build-type "${{ matrix.build_type }}" \
 | 
			
		||||
            build
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										81
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,15 +13,14 @@ on:
 | 
			
		||||
      - ".github/workflows/ci.yml"
 | 
			
		||||
      - "!.yamllint"
 | 
			
		||||
      - "!.github/dependabot.yml"
 | 
			
		||||
      - "!docker/**"
 | 
			
		||||
  merge_group:
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  DEFAULT_PYTHON: "3.10"
 | 
			
		||||
  PYUPGRADE_TARGET: "--py310-plus"
 | 
			
		||||
  DEFAULT_PYTHON: "3.9"
 | 
			
		||||
  PYUPGRADE_TARGET: "--py39-plus"
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -36,18 +35,18 @@ jobs:
 | 
			
		||||
      cache-key: ${{ steps.cache-key.outputs.key }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
      - name: Set up Python ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v4.2.3
 | 
			
		||||
        uses: actions/cache@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -58,26 +57,26 @@ jobs:
 | 
			
		||||
          python -m venv venv
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          python --version
 | 
			
		||||
          pip install -r requirements.txt -r requirements_test.txt
 | 
			
		||||
          pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
          pip install -e .
 | 
			
		||||
 | 
			
		||||
  ruff:
 | 
			
		||||
    name: Check ruff
 | 
			
		||||
  black:
 | 
			
		||||
    name: Check black
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Run Ruff
 | 
			
		||||
      - name: Run black
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ruff format esphome tests
 | 
			
		||||
          black --verbose esphome tests
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
@@ -89,7 +88,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -110,7 +109,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -131,7 +130,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -152,7 +151,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -165,7 +164,6 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          script/ci-custom.py
 | 
			
		||||
          script/build_codeowners.py --check
 | 
			
		||||
          script/build_language_schema.py --check
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    name: Run pytest
 | 
			
		||||
@@ -173,10 +171,10 @@ jobs:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version:
 | 
			
		||||
          - "3.9"
 | 
			
		||||
          - "3.10"
 | 
			
		||||
          - "3.11"
 | 
			
		||||
          - "3.12"
 | 
			
		||||
          - "3.13"
 | 
			
		||||
        os:
 | 
			
		||||
          - ubuntu-latest
 | 
			
		||||
          - macOS-latest
 | 
			
		||||
@@ -185,24 +183,24 @@ jobs:
 | 
			
		||||
          # Minimize CI resource usage
 | 
			
		||||
          # by only running the Python version
 | 
			
		||||
          # version used for docker images on Windows and macOS
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.12"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.10"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
          - python-version: "3.9"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.12"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
          - python-version: "3.10"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
          - python-version: "3.9"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -214,14 +212,14 @@ jobs:
 | 
			
		||||
        if: matrix.os == 'windows-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          ./venv/Scripts/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native -n auto tests
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native -n auto tests
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
      - name: Upload coverage to Codecov
 | 
			
		||||
        uses: codecov/codecov-action@v5.4.3
 | 
			
		||||
        uses: codecov/codecov-action@v5
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
 | 
			
		||||
@@ -232,7 +230,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -256,7 +254,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - ruff
 | 
			
		||||
      - black
 | 
			
		||||
      - ci-custom
 | 
			
		||||
      - clang-format
 | 
			
		||||
      - flake8
 | 
			
		||||
@@ -292,15 +290,10 @@ jobs:
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 IDF
 | 
			
		||||
            options: --environment esp32-idf-tidy --grep USE_ESP_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:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -309,14 +302,14 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@v4.2.3
 | 
			
		||||
        uses: actions/cache@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@v4.2.3
 | 
			
		||||
        uses: actions/cache/restore@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}
 | 
			
		||||
@@ -336,13 +329,13 @@ jobs:
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
 | 
			
		||||
          script/clang-tidy --all-headers --fix ${{ matrix.options }}
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
@@ -356,7 +349,7 @@ jobs:
 | 
			
		||||
      count: ${{ steps.list-components.outputs.count }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        with:
 | 
			
		||||
          # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
 | 
			
		||||
          fetch-depth: 500
 | 
			
		||||
@@ -406,7 +399,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -432,7 +425,7 @@ jobs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Split components into 20 groups
 | 
			
		||||
        id: split
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -462,7 +455,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -488,7 +481,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - ruff
 | 
			
		||||
      - black
 | 
			
		||||
      - ci-custom
 | 
			
		||||
      - clang-format
 | 
			
		||||
      - flake8
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/matchers/lint-python.json
									
									
									
									
										vendored
									
									
								
							@@ -1,11 +1,11 @@
 | 
			
		||||
{
 | 
			
		||||
  "problemMatcher": [
 | 
			
		||||
    {
 | 
			
		||||
      "owner": "ruff",
 | 
			
		||||
      "owner": "black",
 | 
			
		||||
      "severity": "error",
 | 
			
		||||
      "pattern": [
 | 
			
		||||
        {
 | 
			
		||||
          "regexp": "^(.*): (Please format this file with the ruff formatter)",
 | 
			
		||||
          "regexp": "^(.*): (Please format this file with the black formatter)",
 | 
			
		||||
          "file": 1,
 | 
			
		||||
          "message": 2
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										133
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										133
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,9 +18,8 @@ jobs:
 | 
			
		||||
    outputs:
 | 
			
		||||
      tag: ${{ steps.tag.outputs.tag }}
 | 
			
		||||
      branch_build: ${{ steps.tag.outputs.branch_build }}
 | 
			
		||||
      deploy_env: ${{ steps.tag.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
        id: tag
 | 
			
		||||
        # yamllint disable rule:line-length
 | 
			
		||||
@@ -28,11 +27,6 @@ jobs:
 | 
			
		||||
          if [[ "${{ github.event_name }}" = "release" ]]; then
 | 
			
		||||
            TAG="${{ github.event.release.tag_name}}"
 | 
			
		||||
            BRANCH_BUILD="false"
 | 
			
		||||
            if [[ "${{ github.event.release.prerelease }}" = "true" ]]; then
 | 
			
		||||
              ENVIRONMENT="beta"
 | 
			
		||||
            else
 | 
			
		||||
              ENVIRONMENT="production"
 | 
			
		||||
            fi
 | 
			
		||||
          else
 | 
			
		||||
            TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p")
 | 
			
		||||
            today="$(date --utc '+%Y%m%d')"
 | 
			
		||||
@@ -41,15 +35,12 @@ jobs:
 | 
			
		||||
            if [[ "$BRANCH" != "dev" ]]; then
 | 
			
		||||
              TAG="${TAG}-${BRANCH}"
 | 
			
		||||
              BRANCH_BUILD="true"
 | 
			
		||||
              ENVIRONMENT=""
 | 
			
		||||
            else
 | 
			
		||||
              BRANCH_BUILD="false"
 | 
			
		||||
              ENVIRONMENT="dev"
 | 
			
		||||
            fi
 | 
			
		||||
          fi
 | 
			
		||||
          echo "tag=${TAG}" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "branch_build=${BRANCH_BUILD}" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "deploy_env=${ENVIRONMENT}" >> $GITHUB_OUTPUT
 | 
			
		||||
        # yamllint enable rule:line-length
 | 
			
		||||
 | 
			
		||||
  deploy-pypi:
 | 
			
		||||
@@ -60,54 +51,57 @@ jobs:
 | 
			
		||||
      contents: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.x"
 | 
			
		||||
      - name: Set up python environment
 | 
			
		||||
        env:
 | 
			
		||||
          ESPHOME_NO_VENV: 1
 | 
			
		||||
        run: script/setup
 | 
			
		||||
      - name: Build
 | 
			
		||||
        run: |-
 | 
			
		||||
          pip3 install build
 | 
			
		||||
          python3 -m build
 | 
			
		||||
      - name: Publish
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@v1.12.4
 | 
			
		||||
        with:
 | 
			
		||||
          skip-existing: true
 | 
			
		||||
        uses: pypa/gh-action-pypi-publish@v1.12.3
 | 
			
		||||
 | 
			
		||||
  deploy-docker:
 | 
			
		||||
    name: Build ESPHome ${{ matrix.platform.arch }}
 | 
			
		||||
    name: Build ESPHome ${{ matrix.platform }}
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      packages: write
 | 
			
		||||
    runs-on: ${{ matrix.platform.os }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs: [init]
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        platform:
 | 
			
		||||
          - arch: amd64
 | 
			
		||||
            os: "ubuntu-24.04"
 | 
			
		||||
          - arch: arm64
 | 
			
		||||
            os: "ubuntu-24.04-arm"
 | 
			
		||||
 | 
			
		||||
          - linux/amd64
 | 
			
		||||
          - linux/arm/v7
 | 
			
		||||
          - linux/arm64
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.10"
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.7.1
 | 
			
		||||
      - name: Set up QEMU
 | 
			
		||||
        if: matrix.platform != 'linux/amd64'
 | 
			
		||||
        uses: docker/setup-qemu-action@v3.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -116,36 +110,45 @@ jobs:
 | 
			
		||||
      - name: Build docker
 | 
			
		||||
        uses: ./.github/actions/build-image
 | 
			
		||||
        with:
 | 
			
		||||
          target: final
 | 
			
		||||
          build_type: docker
 | 
			
		||||
          platform: ${{ matrix.platform }}
 | 
			
		||||
          target: docker
 | 
			
		||||
          baseimg: docker
 | 
			
		||||
          suffix: ""
 | 
			
		||||
          version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Build ha-addon
 | 
			
		||||
        uses: ./.github/actions/build-image
 | 
			
		||||
        with:
 | 
			
		||||
          target: final
 | 
			
		||||
          build_type: ha-addon
 | 
			
		||||
          platform: ${{ matrix.platform }}
 | 
			
		||||
          target: hassio
 | 
			
		||||
          baseimg: hassio
 | 
			
		||||
          suffix: "hassio"
 | 
			
		||||
          version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      # - name: Build lint
 | 
			
		||||
      #   uses: ./.github/actions/build-image
 | 
			
		||||
      #   with:
 | 
			
		||||
      #     target: lint
 | 
			
		||||
      #     build_type: lint
 | 
			
		||||
      #     suffix: lint
 | 
			
		||||
      #     version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
      - name: Build lint
 | 
			
		||||
        uses: ./.github/actions/build-image
 | 
			
		||||
        with:
 | 
			
		||||
          platform: ${{ matrix.platform }}
 | 
			
		||||
          target: lint
 | 
			
		||||
          baseimg: docker
 | 
			
		||||
          suffix: lint
 | 
			
		||||
          version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Sanitize platform name
 | 
			
		||||
        id: sanitize
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "${{ matrix.platform }}" | sed 's|/|-|g' > /tmp/platform
 | 
			
		||||
          echo name=$(cat /tmp/platform) >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
      - name: Upload digests
 | 
			
		||||
        uses: actions/upload-artifact@v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@v4.4.3
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ matrix.platform.arch }}
 | 
			
		||||
          name: digests-${{ steps.sanitize.outputs.name }}
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  deploy-manifest:
 | 
			
		||||
    name: Publish ESPHome ${{ matrix.image.build_type }} to ${{ matrix.registry }}
 | 
			
		||||
    name: Publish ESPHome ${{ matrix.image.title }} to ${{ matrix.registry }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - init
 | 
			
		||||
@@ -158,37 +161,40 @@ jobs:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        image:
 | 
			
		||||
          - build_type: "docker"
 | 
			
		||||
            suffix: ""
 | 
			
		||||
          - build_type: "ha-addon"
 | 
			
		||||
          - title: "ha-addon"
 | 
			
		||||
            target: "hassio"
 | 
			
		||||
            suffix: "hassio"
 | 
			
		||||
            # - build_type: "lint"
 | 
			
		||||
            #   suffix: "lint"
 | 
			
		||||
          - title: "docker"
 | 
			
		||||
            target: "docker"
 | 
			
		||||
            suffix: ""
 | 
			
		||||
          - title: "lint"
 | 
			
		||||
            target: "lint"
 | 
			
		||||
            suffix: "lint"
 | 
			
		||||
        registry:
 | 
			
		||||
          - ghcr
 | 
			
		||||
          - dockerhub
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v4.3.0
 | 
			
		||||
        uses: actions/download-artifact@v4.1.8
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
          merge-multiple: true
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.7.1
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        if: matrix.registry == 'dockerhub'
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        if: matrix.registry == 'ghcr'
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        uses: docker/login-action@v3.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -207,7 +213,7 @@ jobs:
 | 
			
		||||
          done
 | 
			
		||||
 | 
			
		||||
      - name: Create manifest list and push
 | 
			
		||||
        working-directory: /tmp/digests/${{ matrix.image.build_type }}/${{ matrix.registry }}
 | 
			
		||||
        working-directory: /tmp/digests/${{ matrix.image.target }}/${{ matrix.registry }}
 | 
			
		||||
        run: |
 | 
			
		||||
          docker buildx imagetools create $(jq -Rcnr 'inputs | . / "," | map("-t " + .) | join(" ")' <<< "${{ steps.tags.outputs.tags}}") \
 | 
			
		||||
            $(printf '${{ steps.tags.outputs.image }}@sha256:%s ' *)
 | 
			
		||||
@@ -238,24 +244,3 @@ jobs:
 | 
			
		||||
                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 }}",
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
			
		||||
  stale:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v9.1.0
 | 
			
		||||
      - uses: actions/stale@v9.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: 90
 | 
			
		||||
          days-before-pr-close: 7
 | 
			
		||||
@@ -37,7 +37,7 @@ jobs:
 | 
			
		||||
  close-issues:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/stale@v9.1.0
 | 
			
		||||
      - uses: actions/stale@v9.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          days-before-pr-stale: -1
 | 
			
		||||
          days-before-pr-close: -1
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,18 +13,18 @@ jobs:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
 | 
			
		||||
      - name: Checkout Home Assistant
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        with:
 | 
			
		||||
          repository: home-assistant/core
 | 
			
		||||
          path: lib/home-assistant
 | 
			
		||||
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        uses: actions/setup-python@v5.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: 3.13
 | 
			
		||||
          python-version: 3.12
 | 
			
		||||
 | 
			
		||||
      - name: Install Home Assistant
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -36,11 +36,11 @@ jobs:
 | 
			
		||||
          python ./script/sync-device_class.py
 | 
			
		||||
 | 
			
		||||
      - name: Commit changes
 | 
			
		||||
        uses: peter-evans/create-pull-request@v7.0.8
 | 
			
		||||
        uses: peter-evans/create-pull-request@v7.0.5
 | 
			
		||||
        with:
 | 
			
		||||
          commit-message: "Synchronise Device Classes from Home Assistant"
 | 
			
		||||
          committer: esphomebot <esphome@openhomefoundation.org>
 | 
			
		||||
          author: esphomebot <esphome@openhomefoundation.org>
 | 
			
		||||
          committer: esphomebot <esphome@nabucasa.com>
 | 
			
		||||
          author: esphomebot <esphome@nabucasa.com>
 | 
			
		||||
          branch: sync/device-classes
 | 
			
		||||
          delete-branch: true
 | 
			
		||||
          title: "Synchronise Device Classes from Home Assistant"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/yaml-lint.yml
									
									
									
									
										vendored
									
									
								
							@@ -18,7 +18,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
      - name: Run yamllint
 | 
			
		||||
        uses: frenck/action-yamllint@v1.5.0
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -143,4 +143,3 @@ sdkconfig.*
 | 
			
		||||
/components
 | 
			
		||||
/managed_components
 | 
			
		||||
 | 
			
		||||
api-docs/
 | 
			
		||||
 
 | 
			
		||||
@@ -4,19 +4,27 @@
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.11.10
 | 
			
		||||
    rev: v0.5.4
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
        args: [--fix]
 | 
			
		||||
      # Run the formatter.
 | 
			
		||||
      - id: ruff-format
 | 
			
		||||
  - repo: https://github.com/psf/black-pre-commit-mirror
 | 
			
		||||
    rev: 24.4.2
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: black
 | 
			
		||||
        args:
 | 
			
		||||
          - --safe
 | 
			
		||||
          - --quiet
 | 
			
		||||
        files: ^((esphome|script|tests)/.+)?[^/]+\.py$
 | 
			
		||||
  - repo: https://github.com/PyCQA/flake8
 | 
			
		||||
    rev: 7.2.0
 | 
			
		||||
    rev: 6.1.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: flake8
 | 
			
		||||
        additional_dependencies:
 | 
			
		||||
          - flake8-docstrings==1.7.0
 | 
			
		||||
          - flake8-docstrings==1.5.0
 | 
			
		||||
          - pydocstyle==5.1.1
 | 
			
		||||
        files: ^(esphome|tests)/.+\.py$
 | 
			
		||||
  - repo: https://github.com/pre-commit/pre-commit-hooks
 | 
			
		||||
@@ -28,12 +36,12 @@ repos:
 | 
			
		||||
          - --branch=release
 | 
			
		||||
          - --branch=beta
 | 
			
		||||
  - repo: https://github.com/asottile/pyupgrade
 | 
			
		||||
    rev: v3.20.0
 | 
			
		||||
    rev: v3.15.2
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: pyupgrade
 | 
			
		||||
        args: [--py310-plus]
 | 
			
		||||
        args: [--py39-plus]
 | 
			
		||||
  - repo: https://github.com/adrienverge/yamllint.git
 | 
			
		||||
    rev: v1.37.1
 | 
			
		||||
    rev: v1.35.1
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: yamllint
 | 
			
		||||
  - repo: https://github.com/pre-commit/mirrors-clang-format
 | 
			
		||||
@@ -45,6 +53,6 @@ repos:
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: pylint
 | 
			
		||||
        name: pylint
 | 
			
		||||
        entry: python3 script/run-in-env.py pylint
 | 
			
		||||
        language: system
 | 
			
		||||
        entry: script/run-in-env.sh pylint
 | 
			
		||||
        language: script
 | 
			
		||||
        types: [python]
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -49,7 +49,6 @@ esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/atm90e26/* @danieltwagner
 | 
			
		||||
esphome/components/atm90e32/* @circuitsetup @descipher
 | 
			
		||||
esphome/components/audio/* @kahrendt
 | 
			
		||||
esphome/components/audio_adc/* @kbx81
 | 
			
		||||
esphome/components/audio_dac/* @kbx81
 | 
			
		||||
esphome/components/axs15231/* @clydebarrow
 | 
			
		||||
esphome/components/b_parasite/* @rbaron
 | 
			
		||||
@@ -93,13 +92,10 @@ esphome/components/captive_portal/* @OttoWinter
 | 
			
		||||
esphome/components/ccs811/* @habbie
 | 
			
		||||
esphome/components/cd74hc4067/* @asoehlke
 | 
			
		||||
esphome/components/ch422g/* @clydebarrow @jesterret
 | 
			
		||||
esphome/components/chsc6x/* @kkosik20
 | 
			
		||||
esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/cm1106/* @andrewjswan
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
esphome/components/combination/* @Cat-Ion @kahrendt
 | 
			
		||||
esphome/components/const/* @esphome/core
 | 
			
		||||
esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/copy/* @OttoWinter
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
@@ -135,11 +131,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
 | 
			
		||||
esphome/components/ens160_i2c/* @latonita
 | 
			
		||||
esphome/components/ens160_spi/* @latonita
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/es7210/* @kahrendt
 | 
			
		||||
esphome/components/es7243e/* @kbx81
 | 
			
		||||
esphome/components/es8156/* @kbx81
 | 
			
		||||
esphome/components/es8311/* @kahrendt @kroimon
 | 
			
		||||
esphome/components/es8388/* @P4uLT
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_client/* @jesserockz
 | 
			
		||||
@@ -152,7 +144,6 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
 | 
			
		||||
esphome/components/esp8266/* @esphome/core
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/event/* @nohat
 | 
			
		||||
esphome/components/event_emitter/* @Rapsssito
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
esphome/components/ezo_pmp/* @carlos-sarmiento
 | 
			
		||||
@@ -171,7 +162,7 @@ esphome/components/gp2y1010au0f/* @zry98
 | 
			
		||||
esphome/components/gp8403/* @jesserockz
 | 
			
		||||
esphome/components/gpio/* @esphome/core
 | 
			
		||||
esphome/components/gpio/one_wire/* @ssieb
 | 
			
		||||
esphome/components/gps/* @coogle @ximex
 | 
			
		||||
esphome/components/gps/* @coogle
 | 
			
		||||
esphome/components/graph/* @synco
 | 
			
		||||
esphome/components/graphical_display_menu/* @MrMDavidson
 | 
			
		||||
esphome/components/gree/* @orestismers
 | 
			
		||||
@@ -238,7 +229,6 @@ esphome/components/kuntze/* @ssieb
 | 
			
		||||
esphome/components/lcd_menu/* @numo68
 | 
			
		||||
esphome/components/ld2410/* @regevbr @sebcaps
 | 
			
		||||
esphome/components/ld2420/* @descipher
 | 
			
		||||
esphome/components/ld2450/* @hareeshmu
 | 
			
		||||
esphome/components/ledc/* @OttoWinter
 | 
			
		||||
esphome/components/libretiny/* @kuba2k2
 | 
			
		||||
esphome/components/libretiny_pwm/* @kuba2k2
 | 
			
		||||
@@ -247,13 +237,11 @@ esphome/components/lightwaverf/* @max246
 | 
			
		||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/lock/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
esphome/components/logger/select/* @clydebarrow
 | 
			
		||||
esphome/components/ltr390/* @latonita @sjtrny
 | 
			
		||||
esphome/components/ltr501/* @latonita
 | 
			
		||||
esphome/components/ltr_als_ps/* @latonita
 | 
			
		||||
esphome/components/lvgl/* @clydebarrow
 | 
			
		||||
esphome/components/m5stack_8angle/* @rnauber
 | 
			
		||||
esphome/components/mapping/* @clydebarrow
 | 
			
		||||
esphome/components/matrix_keypad/* @ssieb
 | 
			
		||||
esphome/components/max17043/* @blacknell
 | 
			
		||||
esphome/components/max31865/* @DAVe3283
 | 
			
		||||
@@ -270,7 +258,6 @@ esphome/components/mcp23x17_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp23xxx_base/* @jesserockz
 | 
			
		||||
esphome/components/mcp2515/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/mcp3204/* @rsumner
 | 
			
		||||
esphome/components/mcp4461/* @p1ngb4ck
 | 
			
		||||
esphome/components/mcp4728/* @berfenger
 | 
			
		||||
esphome/components/mcp47a1/* @jesserockz
 | 
			
		||||
esphome/components/mcp9600/* @mreditor97
 | 
			
		||||
@@ -280,13 +267,11 @@ esphome/components/mdns/* @esphome/core
 | 
			
		||||
esphome/components/media_player/* @jesserockz
 | 
			
		||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/micronova/* @jorre05
 | 
			
		||||
esphome/components/microphone/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/microphone/* @jesserockz
 | 
			
		||||
esphome/components/mics_4514/* @jesserockz
 | 
			
		||||
esphome/components/midea/* @dudanov
 | 
			
		||||
esphome/components/midea_ir/* @dudanov
 | 
			
		||||
esphome/components/mipi_spi/* @clydebarrow
 | 
			
		||||
esphome/components/mitsubishi/* @RubyBailey
 | 
			
		||||
esphome/components/mixer/speaker/* @kahrendt
 | 
			
		||||
esphome/components/mlx90393/* @functionpointer
 | 
			
		||||
esphome/components/mlx90614/* @jesserockz
 | 
			
		||||
esphome/components/mmc5603/* @benhoff
 | 
			
		||||
@@ -305,7 +290,6 @@ esphome/components/mopeka_std_check/* @Fabian-Schmidt
 | 
			
		||||
esphome/components/mpl3115a2/* @kbickar
 | 
			
		||||
esphome/components/mpu6886/* @fabaff
 | 
			
		||||
esphome/components/ms8607/* @e28eta
 | 
			
		||||
esphome/components/msa3xx/* @latonita
 | 
			
		||||
esphome/components/nau7802/* @cujomalainey
 | 
			
		||||
esphome/components/network/* @esphome/core
 | 
			
		||||
esphome/components/nextion/* @edwardtfn @senexcrenshaw
 | 
			
		||||
@@ -318,11 +302,10 @@ esphome/components/noblex/* @AGalfra
 | 
			
		||||
esphome/components/npi19/* @bakerkj
 | 
			
		||||
esphome/components/number/* @esphome/core
 | 
			
		||||
esphome/components/one_wire/* @ssieb
 | 
			
		||||
esphome/components/online_image/* @clydebarrow @guillempages
 | 
			
		||||
esphome/components/online_image/* @guillempages
 | 
			
		||||
esphome/components/opentherm/* @olegtarasov
 | 
			
		||||
esphome/components/ota/* @esphome/core
 | 
			
		||||
esphome/components/output/* @esphome/core
 | 
			
		||||
esphome/components/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/pca6416a/* @Mat931
 | 
			
		||||
esphome/components/pca9554/* @clydebarrow @hwstar
 | 
			
		||||
esphome/components/pcf85063/* @brogon
 | 
			
		||||
@@ -330,9 +313,7 @@ esphome/components/pcf8563/* @KoenBreeman
 | 
			
		||||
esphome/components/pid/* @OttoWinter
 | 
			
		||||
esphome/components/pipsolar/* @andreashergert1984
 | 
			
		||||
esphome/components/pm1006/* @habbie
 | 
			
		||||
esphome/components/pm2005/* @andrewjswan
 | 
			
		||||
esphome/components/pmsa003i/* @sjtrny
 | 
			
		||||
esphome/components/pmsx003/* @ximex
 | 
			
		||||
esphome/components/pmwcs3/* @SeByDocKy
 | 
			
		||||
esphome/components/pn532/* @OttoWinter @jesserockz
 | 
			
		||||
esphome/components/pn532_i2c/* @OttoWinter @jesserockz
 | 
			
		||||
@@ -357,7 +338,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
 | 
			
		||||
esphome/components/rc522/* @glmnet
 | 
			
		||||
esphome/components/rc522_i2c/* @glmnet
 | 
			
		||||
esphome/components/rc522_spi/* @glmnet
 | 
			
		||||
esphome/components/resampler/speaker/* @kahrendt
 | 
			
		||||
esphome/components/resistance_sampler/* @jesserockz
 | 
			
		||||
esphome/components/restart/* @esphome/core
 | 
			
		||||
esphome/components/rf_bridge/* @jesserockz
 | 
			
		||||
esphome/components/rgbct/* @jesserockz
 | 
			
		||||
@@ -370,7 +351,7 @@ esphome/components/rtttl/* @glmnet
 | 
			
		||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
 | 
			
		||||
esphome/components/scd4x/* @martgras @sjtrny
 | 
			
		||||
esphome/components/script/* @esphome/core
 | 
			
		||||
esphome/components/sdl/* @bdm310 @clydebarrow
 | 
			
		||||
esphome/components/sdl/* @clydebarrow
 | 
			
		||||
esphome/components/sdm_meter/* @jesserockz @polyfaces
 | 
			
		||||
esphome/components/sdp3x/* @Azimath
 | 
			
		||||
esphome/components/seeed_mr24hpc1/* @limengdu
 | 
			
		||||
@@ -401,9 +382,7 @@ esphome/components/smt100/* @piechade
 | 
			
		||||
esphome/components/sn74hc165/* @jesserockz
 | 
			
		||||
esphome/components/socket/* @esphome/core
 | 
			
		||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
 | 
			
		||||
esphome/components/sound_level/* @kahrendt
 | 
			
		||||
esphome/components/speaker/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
 | 
			
		||||
esphome/components/spi/* @clydebarrow @esphome/core
 | 
			
		||||
esphome/components/spi_device/* @clydebarrow
 | 
			
		||||
esphome/components/spi_led_strip/* @clydebarrow
 | 
			
		||||
@@ -433,7 +412,6 @@ esphome/components/sun/* @OttoWinter
 | 
			
		||||
esphome/components/sun_gtil2/* @Mat931
 | 
			
		||||
esphome/components/switch/* @esphome/core
 | 
			
		||||
esphome/components/switch/binary_sensor/* @ssieb
 | 
			
		||||
esphome/components/syslog/* @clydebarrow
 | 
			
		||||
esphome/components/t6615/* @tylermenezes
 | 
			
		||||
esphome/components/tc74/* @sethgirvan
 | 
			
		||||
esphome/components/tca9548a/* @andreashergert1984
 | 
			
		||||
@@ -459,7 +437,6 @@ esphome/components/tmp102/* @timsavage
 | 
			
		||||
esphome/components/tmp1075/* @sybrenstuvel
 | 
			
		||||
esphome/components/tmp117/* @Azimath
 | 
			
		||||
esphome/components/tof10120/* @wstrzalka
 | 
			
		||||
esphome/components/tormatic/* @ti-mo
 | 
			
		||||
esphome/components/toshiba/* @kbx81
 | 
			
		||||
esphome/components/touchscreen/* @jesserockz @nielsnl68
 | 
			
		||||
esphome/components/tsl2591/* @wjcarpenter
 | 
			
		||||
@@ -473,15 +450,12 @@ esphome/components/tuya/switch/* @jesserockz
 | 
			
		||||
esphome/components/tuya/text_sensor/* @dentra
 | 
			
		||||
esphome/components/uart/* @esphome/core
 | 
			
		||||
esphome/components/uart/button/* @ssieb
 | 
			
		||||
esphome/components/uart/packet_transport/* @clydebarrow
 | 
			
		||||
esphome/components/udp/* @clydebarrow
 | 
			
		||||
esphome/components/ufire_ec/* @pvizeli
 | 
			
		||||
esphome/components/ufire_ise/* @pvizeli
 | 
			
		||||
esphome/components/ultrasonic/* @OttoWinter
 | 
			
		||||
esphome/components/update/* @jesserockz
 | 
			
		||||
esphome/components/uponor_smatrix/* @kroimon
 | 
			
		||||
esphome/components/usb_host/* @clydebarrow
 | 
			
		||||
esphome/components/usb_uart/* @clydebarrow
 | 
			
		||||
esphome/components/valve/* @esphome/core
 | 
			
		||||
esphome/components/vbus/* @ssieb
 | 
			
		||||
esphome/components/veml3235/* @kbx81
 | 
			
		||||
@@ -519,6 +493,5 @@ esphome/components/xiaomi_mhoc401/* @vevsvevs
 | 
			
		||||
esphome/components/xiaomi_rtcgq02lm/* @jesserockz
 | 
			
		||||
esphome/components/xl9535/* @mreditor97
 | 
			
		||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
 | 
			
		||||
esphome/components/xxtea/* @clydebarrow
 | 
			
		||||
esphome/components/zhlt01/* @cfeenstra1024
 | 
			
		||||
esphome/components/zio_ultrasonic/* @kahrendt
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ This Code of Conduct applies both within project spaces and in public spaces whe
 | 
			
		||||
 | 
			
		||||
## Enforcement
 | 
			
		||||
 | 
			
		||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@openhomefoundation.org. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
 | 
			
		||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at esphome@nabucasa.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
 | 
			
		||||
 | 
			
		||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,12 @@
 | 
			
		||||
# Contributing to ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
 | 
			
		||||
# Contributing to ESPHome
 | 
			
		||||
 | 
			
		||||
We welcome contributions to the ESPHome suite of code and documentation!
 | 
			
		||||
For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome
 | 
			
		||||
 | 
			
		||||
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
 | 
			
		||||
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
 | 
			
		||||
Things to note when contributing:
 | 
			
		||||
 | 
			
		||||
**See also:**
 | 
			
		||||
 | 
			
		||||
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
 | 
			
		||||
[](https://www.openhomefoundation.org/)
 | 
			
		||||
 - Please test your changes :)
 | 
			
		||||
 - If a new feature is added or an existing user-facing feature is changed, you should also
 | 
			
		||||
   update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs)
 | 
			
		||||
   for more information.
 | 
			
		||||
 - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files
 | 
			
		||||
   which checks if your new feature compiles correctly.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										13
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								README.md
									
									
									
									
									
								
							@@ -1,16 +1,11 @@
 | 
			
		||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
 | 
			
		||||
 | 
			
		||||
<a href="https://esphome.io/">
 | 
			
		||||
  <picture>
 | 
			
		||||
    <source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
 | 
			
		||||
    <img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
 | 
			
		||||
  </picture>
 | 
			
		||||
</a>
 | 
			
		||||
[](https://esphome.io/)
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
**Documentation:** https://esphome.io/
 | 
			
		||||
 | 
			
		||||
[Documentation](https://esphome.io) -- [Issues](https://github.com/esphome/issues/issues) -- [Feature requests](https://github.com/esphome/feature-requests/issues)
 | 
			
		||||
For issues, please go to [the issue tracker](https://github.com/esphome/issues/issues).
 | 
			
		||||
 | 
			
		||||
---
 | 
			
		||||
For feature requests, please see [feature requests](https://github.com/esphome/feature-requests/issues).
 | 
			
		||||
 | 
			
		||||
[](https://www.openhomefoundation.org/)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,56 +1,155 @@
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
ARG BUILD_OS=alpine
 | 
			
		||||
ARG BUILD_BASE_VERSION=2025.04.0
 | 
			
		||||
ARG BUILD_TYPE=docker
 | 
			
		||||
# Build these with the build.py script
 | 
			
		||||
# Example:
 | 
			
		||||
#   python3 docker/build.py --tag dev --arch amd64 --build-type docker build
 | 
			
		||||
 | 
			
		||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-${BUILD_BASE_VERSION} AS base-source-docker
 | 
			
		||||
FROM ghcr.io/esphome/docker-base:${BUILD_OS}-ha-addon-${BUILD_BASE_VERSION} AS base-source-ha-addon
 | 
			
		||||
# One of "docker", "hassio"
 | 
			
		||||
ARG BASEIMGTYPE=docker
 | 
			
		||||
 | 
			
		||||
ARG BUILD_TYPE
 | 
			
		||||
FROM base-source-${BUILD_TYPE} AS base
 | 
			
		||||
 | 
			
		||||
RUN git config --system --add safe.directory "*"
 | 
			
		||||
# https://github.com/hassio-addons/addon-debian-base/releases
 | 
			
		||||
FROM ghcr.io/hassio-addons/debian-base:7.2.0 AS base-hassio
 | 
			
		||||
# https://hub.docker.com/_/debian?tab=tags&page=1&name=bookworm
 | 
			
		||||
FROM debian:12.2-slim AS base-docker
 | 
			
		||||
 | 
			
		||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
 | 
			
		||||
FROM base-${BASEIMGTYPE} AS base
 | 
			
		||||
 | 
			
		||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt /
 | 
			
		||||
ARG TARGETARCH
 | 
			
		||||
ARG TARGETVARIANT
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Note that --break-system-packages is used below because
 | 
			
		||||
# https://peps.python.org/pep-0668/ added a safety check that prevents
 | 
			
		||||
# installing packages with the same name as a system package. This is
 | 
			
		||||
# not a problem for us because we are not concerned about overwriting
 | 
			
		||||
# system packages because we are running in an isolated container.
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    uv pip install --no-cache-dir \
 | 
			
		||||
    -r /requirements.txt
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        python3-pip=23.0.1+dfsg-1 \
 | 
			
		||||
        python3-setuptools=66.1.1-1 \
 | 
			
		||||
        python3-venv=3.11.2-1+b1 \
 | 
			
		||||
        python3-wheel=0.38.4-2 \
 | 
			
		||||
        iputils-ping=3:20221126-1+deb12u1 \
 | 
			
		||||
        git=1:2.39.5-0+deb12u1 \
 | 
			
		||||
        curl=7.88.1-10+deb12u8 \
 | 
			
		||||
        openssh-client=1:9.2p1-2+deb12u3 \
 | 
			
		||||
        python3-cffi=1.15.1-5 \
 | 
			
		||||
        libcairo2=1.16.0-7 \
 | 
			
		||||
        libmagic1=1:5.44-3 \
 | 
			
		||||
        patch=2.7.6-7 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/
 | 
			
		||||
  LANG=C.UTF-8 LC_ALL=C.UTF-8 \
 | 
			
		||||
  # Store globally installed pio libs in /piolibs
 | 
			
		||||
  PLATFORMIO_GLOBALLIB_DIR=/piolibs
 | 
			
		||||
 | 
			
		||||
# Support legacy binaries on Debian multiarch system. There is no "correct" way
 | 
			
		||||
# to do this, other than using properly built toolchains...
 | 
			
		||||
# See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian
 | 
			
		||||
RUN \
 | 
			
		||||
    if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        ln -s /lib/arm-linux-gnueabihf/ld-linux-armhf.so.3 /lib/ld-linux.so.3; \
 | 
			
		||||
    fi
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    platformio settings set enable_telemetry No \
 | 
			
		||||
    # Ubuntu python3-pip is missing wheel
 | 
			
		||||
    if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
 | 
			
		||||
    fi; \
 | 
			
		||||
    pip3 install \
 | 
			
		||||
    --break-system-packages --no-cache-dir \
 | 
			
		||||
    # Keep platformio version in sync with requirements.txt
 | 
			
		||||
    platformio==6.1.16 \
 | 
			
		||||
    # Change some platformio settings
 | 
			
		||||
    && platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_platformio_interval 1000000 \
 | 
			
		||||
    && mkdir -p /piolibs
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# First install requirements to leverage caching when requirements don't change
 | 
			
		||||
# tmpfs is for https://github.com/rust-lang/cargo/issues/8719
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt requirements_optional.txt /
 | 
			
		||||
RUN --mount=type=tmpfs,target=/root/.cargo <<END-OF-RUN
 | 
			
		||||
# Fail on any non-zero status
 | 
			
		||||
set -e
 | 
			
		||||
 | 
			
		||||
if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
 | 
			
		||||
then
 | 
			
		||||
    curl -L https://www.piwheels.org/cp311/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl -o /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
 | 
			
		||||
    pip3 install --break-system-packages --no-cache-dir /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
 | 
			
		||||
    rm /tmp/cryptography-43.0.0-cp37-abi3-linux_armv7l.whl
 | 
			
		||||
    export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple";
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# install build tools in case wheels are not available
 | 
			
		||||
BUILD_DEPS="
 | 
			
		||||
    build-essential=12.9
 | 
			
		||||
    python3-dev=3.11.2-1+b1
 | 
			
		||||
    zlib1g-dev=1:1.2.13.dfsg-1
 | 
			
		||||
    libjpeg-dev=1:2.1.5-2
 | 
			
		||||
    libfreetype-dev=2.12.1+dfsg-5+deb12u3
 | 
			
		||||
    libssl-dev=3.0.15-1~deb12u1
 | 
			
		||||
    libffi-dev=3.4.4-1
 | 
			
		||||
    cargo=0.66.0+ds1-1
 | 
			
		||||
    pkg-config=1.8.1-1
 | 
			
		||||
"
 | 
			
		||||
LIB_DEPS="
 | 
			
		||||
    libtiff6=4.5.0-6+deb12u1
 | 
			
		||||
    libopenjp2-7=2.5.0-2
 | 
			
		||||
"
 | 
			
		||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
 | 
			
		||||
then
 | 
			
		||||
    apt-get update
 | 
			
		||||
    apt-get install -y --no-install-recommends $BUILD_DEPS $LIB_DEPS
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
CARGO_REGISTRIES_CRATES_IO_PROTOCOL=sparse CARGO_HOME=/root/.cargo
 | 
			
		||||
pip3 install --break-system-packages --no-cache-dir -r /requirements.txt -r /requirements_optional.txt
 | 
			
		||||
 | 
			
		||||
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ] || [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]
 | 
			
		||||
then
 | 
			
		||||
    apt-get remove -y --purge --auto-remove $BUILD_DEPS
 | 
			
		||||
    rm -rf /tmp/* /var/{cache,log}/* /var/lib/apt/lists/*
 | 
			
		||||
fi
 | 
			
		||||
END-OF-RUN
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
COPY script/platformio_install_deps.py platformio.ini /
 | 
			
		||||
RUN /platformio_install_deps.py /platformio.ini --libraries
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION
 | 
			
		||||
 | 
			
		||||
LABEL \
 | 
			
		||||
    org.opencontainers.image.authors="The ESPHome Authors" \
 | 
			
		||||
    org.opencontainers.image.title="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
    org.opencontainers.image.url="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.documentation="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.source="https://github.com/esphome/esphome" \
 | 
			
		||||
    org.opencontainers.image.licenses="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.version=${BUILD_VERSION}
 | 
			
		||||
# Avoid unsafe git error when container user and file config volume permissions don't match
 | 
			
		||||
RUN git config --system --add safe.directory '*'
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= docker-type image =======================
 | 
			
		||||
FROM base AS base-docker
 | 
			
		||||
FROM base AS docker
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
 | 
			
		||||
  fi; \
 | 
			
		||||
  pip3 install \
 | 
			
		||||
  --break-system-packages --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Settings for dashboard
 | 
			
		||||
ENV USERNAME="" PASSWORD=""
 | 
			
		||||
 | 
			
		||||
# Expose the dashboard to Docker
 | 
			
		||||
EXPOSE 6052
 | 
			
		||||
 | 
			
		||||
# Run healthcheck (heartbeat)
 | 
			
		||||
HEALTHCHECK --interval=30s --timeout=30s \
 | 
			
		||||
    CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
 | 
			
		||||
  CMD curl --fail http://localhost:6052/version -A "HealthCheck" || exit 1
 | 
			
		||||
 | 
			
		||||
COPY docker/docker_entrypoint.sh /entrypoint.sh
 | 
			
		||||
 | 
			
		||||
@@ -64,13 +163,47 @@ ENTRYPOINT ["/entrypoint.sh"]
 | 
			
		||||
CMD ["dashboard", "/config"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= ha-addon-type image =======================
 | 
			
		||||
FROM base AS base-ha-addon
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
LABEL \
 | 
			
		||||
    org.opencontainers.image.authors="The ESPHome Authors" \
 | 
			
		||||
    org.opencontainers.image.title="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
    org.opencontainers.image.url="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.documentation="https://esphome.io/" \
 | 
			
		||||
    org.opencontainers.image.source="https://github.com/esphome/esphome" \
 | 
			
		||||
    org.opencontainers.image.licenses="ESPHome" \
 | 
			
		||||
    org.opencontainers.image.version=${BUILD_VERSION}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= hassio-type image =======================
 | 
			
		||||
FROM base AS hassio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        nginx-light=1.22.1-9 \
 | 
			
		||||
    && rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION=dev
 | 
			
		||||
 | 
			
		||||
# Copy root filesystem
 | 
			
		||||
COPY docker/ha-addon-rootfs/ /
 | 
			
		||||
 | 
			
		||||
ARG BUILD_VERSION
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
 | 
			
		||||
  fi; \
 | 
			
		||||
  pip3 install \
 | 
			
		||||
  --break-system-packages --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
# Labels
 | 
			
		||||
LABEL \
 | 
			
		||||
    io.hass.name="ESPHome" \
 | 
			
		||||
    io.hass.description="ESPHome is a system to configure your microcontrollers by simple yet powerful configuration files and control them remotely through Home Automation systems" \
 | 
			
		||||
@@ -78,9 +211,42 @@ LABEL \
 | 
			
		||||
    io.hass.version="${BUILD_VERSION}"
 | 
			
		||||
    # io.hass.arch is inherited from addon-debian-base
 | 
			
		||||
 | 
			
		||||
ARG BUILD_TYPE
 | 
			
		||||
FROM base-${BUILD_TYPE} AS final
 | 
			
		||||
 | 
			
		||||
# Copy esphome and install
 | 
			
		||||
COPY . /esphome
 | 
			
		||||
RUN uv pip install --no-cache-dir -e /esphome
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# ======================= lint-type image =======================
 | 
			
		||||
FROM base AS lint
 | 
			
		||||
 | 
			
		||||
ENV \
 | 
			
		||||
  PLATFORMIO_CORE_DIR=/esphome/.temp/platformio
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    curl -L https://apt.llvm.org/llvm-snapshot.gpg.key -o /etc/apt/trusted.gpg.d/apt.llvm.org.asc \
 | 
			
		||||
    && echo "deb http://apt.llvm.org/bookworm/ llvm-toolchain-bookworm-18 main" > /etc/apt/sources.list.d/llvm.sources.list \
 | 
			
		||||
    && apt-get update \
 | 
			
		||||
    # Use pinned versions so that we get updates with build caching
 | 
			
		||||
    && apt-get install -y --no-install-recommends \
 | 
			
		||||
        clang-format-13=1:13.0.1-11+b2 \
 | 
			
		||||
        patch=2.7.6-7 \
 | 
			
		||||
        software-properties-common=0.99.30-4.1~deb12u1 \
 | 
			
		||||
        nano=7.2-1+deb12u1 \
 | 
			
		||||
        build-essential=12.9 \
 | 
			
		||||
        python3-dev=3.11.2-1+b1 \
 | 
			
		||||
    && if [ "$TARGETARCH$TARGETVARIANT" != "armv7" ]; then \
 | 
			
		||||
    # move this up after armv7 is retired
 | 
			
		||||
    apt-get install -y --no-install-recommends clang-tidy-18=1:18.1.8~++20240731024826+3b5b5c1ec4a3-1~exp1~20240731144843.145 ; \
 | 
			
		||||
    fi; \
 | 
			
		||||
    rm -rf \
 | 
			
		||||
        /tmp/* \
 | 
			
		||||
        /var/{cache,log}/* \
 | 
			
		||||
        /var/lib/apt/lists/*
 | 
			
		||||
 | 
			
		||||
COPY requirements_test.txt /
 | 
			
		||||
RUN if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \
 | 
			
		||||
        export PIP_EXTRA_INDEX_URL="https://www.piwheels.org/simple"; \
 | 
			
		||||
  fi; \
 | 
			
		||||
  pip3 install \
 | 
			
		||||
  --break-system-packages --no-cache-dir -r /requirements_test.txt
 | 
			
		||||
 | 
			
		||||
VOLUME ["/esphome"]
 | 
			
		||||
WORKDIR /esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,22 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import argparse
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import re
 | 
			
		||||
import shlex
 | 
			
		||||
import subprocess
 | 
			
		||||
import argparse
 | 
			
		||||
from platform import machine
 | 
			
		||||
import shlex
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CHANNEL_DEV = "dev"
 | 
			
		||||
CHANNEL_BETA = "beta"
 | 
			
		||||
CHANNEL_RELEASE = "release"
 | 
			
		||||
CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE]
 | 
			
		||||
 | 
			
		||||
ARCH_AMD64 = "amd64"
 | 
			
		||||
ARCH_ARMV7 = "armv7"
 | 
			
		||||
ARCH_AARCH64 = "aarch64"
 | 
			
		||||
ARCHS = [ARCH_AMD64, ARCH_AARCH64]
 | 
			
		||||
ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64]
 | 
			
		||||
 | 
			
		||||
TYPE_DOCKER = "docker"
 | 
			
		||||
TYPE_HA_ADDON = "ha-addon"
 | 
			
		||||
@@ -54,7 +57,7 @@ manifest_parser = subparsers.add_parser(
 | 
			
		||||
class DockerParams:
 | 
			
		||||
    build_to: str
 | 
			
		||||
    manifest_to: str
 | 
			
		||||
    build_type: str
 | 
			
		||||
    baseimgtype: str
 | 
			
		||||
    platform: str
 | 
			
		||||
    target: str
 | 
			
		||||
 | 
			
		||||
@@ -66,19 +69,25 @@ class DockerParams:
 | 
			
		||||
            TYPE_LINT: "esphome/esphome-lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        build_to = f"{prefix}-{arch}"
 | 
			
		||||
        baseimgtype = {
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_LINT: "docker",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        platform = {
 | 
			
		||||
            ARCH_AMD64: "linux/amd64",
 | 
			
		||||
            ARCH_ARMV7: "linux/arm/v7",
 | 
			
		||||
            ARCH_AARCH64: "linux/arm64",
 | 
			
		||||
        }[arch]
 | 
			
		||||
        target = {
 | 
			
		||||
            TYPE_DOCKER: "final",
 | 
			
		||||
            TYPE_HA_ADDON: "final",
 | 
			
		||||
            TYPE_DOCKER: "docker",
 | 
			
		||||
            TYPE_HA_ADDON: "hassio",
 | 
			
		||||
            TYPE_LINT: "lint",
 | 
			
		||||
        }[build_type]
 | 
			
		||||
        return cls(
 | 
			
		||||
            build_to=build_to,
 | 
			
		||||
            manifest_to=prefix,
 | 
			
		||||
            build_type=build_type,
 | 
			
		||||
            baseimgtype=baseimgtype,
 | 
			
		||||
            platform=platform,
 | 
			
		||||
            target=target,
 | 
			
		||||
        )
 | 
			
		||||
@@ -140,7 +149,7 @@ def main():
 | 
			
		||||
            "buildx",
 | 
			
		||||
            "build",
 | 
			
		||||
            "--build-arg",
 | 
			
		||||
            f"BUILD_TYPE={params.build_type}",
 | 
			
		||||
            f"BASEIMGTYPE={params.baseimgtype}",
 | 
			
		||||
            "--build-arg",
 | 
			
		||||
            f"BUILD_VERSION={args.tag}",
 | 
			
		||||
            "--cache-from",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#!/usr/bin/env python3
 | 
			
		||||
import argparse
 | 
			
		||||
import re
 | 
			
		||||
import argparse
 | 
			
		||||
 | 
			
		||||
CHANNEL_DEV = "dev"
 | 
			
		||||
CHANNEL_BETA = "beta"
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,10 @@ if bashio::config.true 'streamer_mode'; then
 | 
			
		||||
    export ESPHOME_STREAMER_MODE=true
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if bashio::config.true 'status_use_ping'; then
 | 
			
		||||
    export ESPHOME_DASHBOARD_USE_PING=true
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if bashio::config.has_value 'relative_url'; then
 | 
			
		||||
    export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url')
 | 
			
		||||
fi
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@
 | 
			
		||||
import argparse
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import functools
 | 
			
		||||
import importlib
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
@@ -43,7 +42,7 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.helpers import get_bool_env, indent, is_ip_address
 | 
			
		||||
from esphome.log import AnsiFore, color, setup_log
 | 
			
		||||
from esphome.log import Fore, color, setup_log
 | 
			
		||||
from esphome.util import (
 | 
			
		||||
    get_serial_ports,
 | 
			
		||||
    list_yaml_files,
 | 
			
		||||
@@ -67,7 +66,7 @@ def choose_prompt(options, purpose: str = None):
 | 
			
		||||
        return options[0][1]
 | 
			
		||||
 | 
			
		||||
    safe_print(
 | 
			
		||||
        f"Found multiple options{f' for {purpose}' if purpose else ''}, please choose one:"
 | 
			
		||||
        f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:'
 | 
			
		||||
    )
 | 
			
		||||
    for i, (desc, _) in enumerate(options):
 | 
			
		||||
        safe_print(f"  [{i + 1}] {desc}")
 | 
			
		||||
@@ -83,7 +82,7 @@ def choose_prompt(options, purpose: str = None):
 | 
			
		||||
                raise ValueError
 | 
			
		||||
            break
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
 | 
			
		||||
            safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
 | 
			
		||||
    return options[opt - 1][1]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -133,7 +132,7 @@ def get_port_type(port):
 | 
			
		||||
    return "NETWORK"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_miniterm(config, port, args):
 | 
			
		||||
def run_miniterm(config, port):
 | 
			
		||||
    import serial
 | 
			
		||||
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
@@ -154,7 +153,7 @@ def run_miniterm(config, port, args):
 | 
			
		||||
 | 
			
		||||
    # We can't set to False by default since it leads to toggling and hence
 | 
			
		||||
    # ESP32 resets on some platforms.
 | 
			
		||||
    if config["logger"][CONF_DEASSERT_RTS_DTR] or args.reset:
 | 
			
		||||
    if config["logger"][CONF_DEASSERT_RTS_DTR]:
 | 
			
		||||
        ser.dtr = False
 | 
			
		||||
        ser.rts = False
 | 
			
		||||
 | 
			
		||||
@@ -244,11 +243,11 @@ def compile_program(args, config):
 | 
			
		||||
    return 0 if idedata is not None else 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_using_esptool(config, port, file, speed):
 | 
			
		||||
def upload_using_esptool(config, port, file):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
 | 
			
		||||
        "upload_speed", os.getenv("ESPHOME_UPLOAD_SPEED", "460800")
 | 
			
		||||
    first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
 | 
			
		||||
        "upload_speed", 460800
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if file is not None:
 | 
			
		||||
@@ -337,18 +336,11 @@ def check_permissions(port):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_program(config, args, host):
 | 
			
		||||
    try:
 | 
			
		||||
        module = importlib.import_module("esphome.components." + CORE.target_platform)
 | 
			
		||||
        if getattr(module, "upload_program")(config, args, host):
 | 
			
		||||
            return 0
 | 
			
		||||
    except AttributeError:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    if get_port_type(host) == "SERIAL":
 | 
			
		||||
        check_permissions(host)
 | 
			
		||||
        if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266):
 | 
			
		||||
            file = getattr(args, "file", None)
 | 
			
		||||
            return upload_using_esptool(config, host, file, args.upload_speed)
 | 
			
		||||
            return upload_using_esptool(config, host, file)
 | 
			
		||||
 | 
			
		||||
        if CORE.target_platform in (PLATFORM_RP2040):
 | 
			
		||||
            return upload_using_platformio(config, args.device)
 | 
			
		||||
@@ -375,12 +367,10 @@ def upload_program(config, args, host):
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        CONF_MQTT in config  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        not is_ip_address(CORE.address)  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        and (get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED])
 | 
			
		||||
        and CONF_MQTT in config
 | 
			
		||||
        and (not args.device or args.device in ("MQTT", "OTA"))
 | 
			
		||||
        and (
 | 
			
		||||
            ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
 | 
			
		||||
            or get_port_type(host) == "MQTT"
 | 
			
		||||
        )
 | 
			
		||||
    ):
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
@@ -399,7 +389,7 @@ def show_logs(config, args, port):
 | 
			
		||||
        raise EsphomeError("Logger is not configured!")
 | 
			
		||||
    if get_port_type(port) == "SERIAL":
 | 
			
		||||
        check_permissions(port)
 | 
			
		||||
        return run_miniterm(config, port, args)
 | 
			
		||||
        return run_miniterm(config, port)
 | 
			
		||||
    if get_port_type(port) == "NETWORK" and "api" in config:
 | 
			
		||||
        if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
 | 
			
		||||
            from esphome import mqtt
 | 
			
		||||
@@ -593,38 +583,33 @@ def command_update_all(args):
 | 
			
		||||
        middle_text = f" {middle_text} "
 | 
			
		||||
        width = len(click.unstyle(middle_text))
 | 
			
		||||
        half_line = "=" * ((twidth - width) // 2)
 | 
			
		||||
        safe_print(f"{half_line}{middle_text}{half_line}")
 | 
			
		||||
        click.echo(f"{half_line}{middle_text}{half_line}")
 | 
			
		||||
 | 
			
		||||
    for f in files:
 | 
			
		||||
        safe_print(f"Updating {color(AnsiFore.CYAN, f)}")
 | 
			
		||||
        safe_print("-" * twidth)
 | 
			
		||||
        safe_print()
 | 
			
		||||
        if CORE.dashboard:
 | 
			
		||||
            rc = run_external_process(
 | 
			
		||||
                "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            rc = run_external_process(
 | 
			
		||||
                "esphome", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
            )
 | 
			
		||||
        print(f"Updating {color(Fore.CYAN, f)}")
 | 
			
		||||
        print("-" * twidth)
 | 
			
		||||
        print()
 | 
			
		||||
        rc = run_external_process(
 | 
			
		||||
            "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
 | 
			
		||||
        )
 | 
			
		||||
        if rc == 0:
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
 | 
			
		||||
            success[f] = True
 | 
			
		||||
        else:
 | 
			
		||||
            print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
 | 
			
		||||
            success[f] = False
 | 
			
		||||
 | 
			
		||||
        safe_print()
 | 
			
		||||
        safe_print()
 | 
			
		||||
        safe_print()
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
        print()
 | 
			
		||||
 | 
			
		||||
    print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
 | 
			
		||||
    print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
 | 
			
		||||
    failed = 0
 | 
			
		||||
    for f in files:
 | 
			
		||||
        if success[f]:
 | 
			
		||||
            safe_print(f"  - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
 | 
			
		||||
            print(f"  - {f}: {color(Fore.GREEN, 'SUCCESS')}")
 | 
			
		||||
        else:
 | 
			
		||||
            safe_print(f"  - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            print(f"  - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
 | 
			
		||||
            failed += 1
 | 
			
		||||
    return failed
 | 
			
		||||
 | 
			
		||||
@@ -650,7 +635,7 @@ def command_rename(args, config):
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
                color(
 | 
			
		||||
                    AnsiFore.BOLD_RED,
 | 
			
		||||
                    Fore.BOLD_RED,
 | 
			
		||||
                    f"'{c}' is an invalid character for names. Valid characters are: "
 | 
			
		||||
                    f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
 | 
			
		||||
                )
 | 
			
		||||
@@ -663,9 +648,7 @@ def command_rename(args, config):
 | 
			
		||||
    yaml = yaml_util.load_yaml(CORE.config_path)
 | 
			
		||||
    if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
 | 
			
		||||
        print(
 | 
			
		||||
            color(
 | 
			
		||||
                AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
 | 
			
		||||
            )
 | 
			
		||||
            color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
 | 
			
		||||
        )
 | 
			
		||||
        return 1
 | 
			
		||||
    old_name = yaml[CONF_ESPHOME][CONF_NAME]
 | 
			
		||||
@@ -688,7 +671,7 @@ def command_rename(args, config):
 | 
			
		||||
            )
 | 
			
		||||
            > 1
 | 
			
		||||
        ):
 | 
			
		||||
            print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
 | 
			
		||||
            return 1
 | 
			
		||||
 | 
			
		||||
        new_raw = re.sub(
 | 
			
		||||
@@ -700,7 +683,7 @@ def command_rename(args, config):
 | 
			
		||||
 | 
			
		||||
    new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
 | 
			
		||||
    print(
 | 
			
		||||
        f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
 | 
			
		||||
        f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
 | 
			
		||||
    )
 | 
			
		||||
    print()
 | 
			
		||||
 | 
			
		||||
@@ -709,7 +692,7 @@ def command_rename(args, config):
 | 
			
		||||
 | 
			
		||||
    rc = run_external_process("esphome", "config", new_path)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
 | 
			
		||||
        os.remove(new_path)
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
@@ -735,7 +718,7 @@ def command_rename(args, config):
 | 
			
		||||
    if CORE.config_path != new_path:
 | 
			
		||||
        os.remove(CORE.config_path)
 | 
			
		||||
 | 
			
		||||
    print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print(color(Fore.BOLD_GREEN, "SUCCESS"))
 | 
			
		||||
    print()
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
@@ -775,14 +758,6 @@ def parse_args(argv):
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "-q", "--quiet", help="Disable all ESPHome logs.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "-l",
 | 
			
		||||
        "--log-level",
 | 
			
		||||
        help="Set the log level.",
 | 
			
		||||
        default=os.getenv("ESPHOME_LOG_LEVEL", "INFO"),
 | 
			
		||||
        action="store",
 | 
			
		||||
        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--dashboard", help=argparse.SUPPRESS, action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
@@ -851,10 +826,6 @@ def parse_args(argv):
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--upload_speed",
 | 
			
		||||
        help="Override the default or configured upload speed.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--file",
 | 
			
		||||
        help="Manually specify the binary file to upload.",
 | 
			
		||||
@@ -873,13 +844,6 @@ def parse_args(argv):
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_logs.add_argument(
 | 
			
		||||
        "--reset",
 | 
			
		||||
        "-r",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        help="Reset the device before starting serial logs.",
 | 
			
		||||
        default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_discover = subparsers.add_parser(
 | 
			
		||||
        "discover",
 | 
			
		||||
@@ -902,20 +866,9 @@ def parse_args(argv):
 | 
			
		||||
        "--device",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--upload_speed",
 | 
			
		||||
        help="Override the default or configured upload speed.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--no-logs", help="Disable starting logs.", action="store_true"
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--reset",
 | 
			
		||||
        "-r",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        help="Reset the device before starting serial logs.",
 | 
			
		||||
        default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser_clean = subparsers.add_parser(
 | 
			
		||||
        "clean-mqtt",
 | 
			
		||||
@@ -1034,16 +987,11 @@ def run_esphome(argv):
 | 
			
		||||
    args = parse_args(argv)
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
 | 
			
		||||
    # Override log level if verbose is set
 | 
			
		||||
    if args.verbose:
 | 
			
		||||
        args.log_level = "DEBUG"
 | 
			
		||||
    elif args.quiet:
 | 
			
		||||
        args.log_level = "CRITICAL"
 | 
			
		||||
 | 
			
		||||
    setup_log(
 | 
			
		||||
        log_level=args.log_level,
 | 
			
		||||
        args.verbose,
 | 
			
		||||
        args.quiet,
 | 
			
		||||
        # Show timestamp for dashboard access logs
 | 
			
		||||
        include_timestamp=args.command == "dashboard",
 | 
			
		||||
        args.command == "dashboard",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if args.command in PRE_CONFIG_ACTIONS:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor, uart
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_DISTANCE,
 | 
			
		||||
    ICON_ARROW_EXPAND_VERTICAL,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_METER,
 | 
			
		||||
    ICON_ARROW_EXPAND_VERTICAL,
 | 
			
		||||
    DEVICE_CLASS_DISTANCE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@MrSuicideParrot"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor, uart
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_DISTANCE,
 | 
			
		||||
    ICON_ARROW_EXPAND_VERTICAL,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ICON_ARROW_EXPAND_VERTICAL,
 | 
			
		||||
    DEVICE_CLASS_DISTANCE,
 | 
			
		||||
    UNIT_MILLIMETER,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace a4988 {
 | 
			
		||||
static const char *const TAG = "a4988.stepper";
 | 
			
		||||
 | 
			
		||||
void A4988::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up A4988...");
 | 
			
		||||
  if (this->sleep_pin_ != nullptr) {
 | 
			
		||||
    this->sleep_pin_->setup();
 | 
			
		||||
    this->sleep_pin_->digital_write(false);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import stepper
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
a4988_ns = cg.esphome_ns.namespace("a4988")
 | 
			
		||||
A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace absolute_humidity {
 | 
			
		||||
static const char *const TAG = "absolute_humidity.sensor";
 | 
			
		||||
 | 
			
		||||
void AbsoluteHumidityComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup for '%s'", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up absolute humidity '%s'...", this->get_name().c_str());
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "  Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
 | 
			
		||||
  this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_EQUATION,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    ICON_WATER,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    CONF_EQUATION,
 | 
			
		||||
    ICON_WATER,
 | 
			
		||||
    UNIT_GRAMS_PER_CUBIC_METER,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -114,14 +114,13 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() {
 | 
			
		||||
    // fully off, disable output immediately
 | 
			
		||||
    this->gate_pin.digital_write(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
    if (this->method == DIM_METHOD_TRAILING) {
 | 
			
		||||
      this->enable_time_us = 1;  // cannot be 0
 | 
			
		||||
      // calculate time until disable in µs with integer arithmetic and take into account min_power
 | 
			
		||||
      this->disable_time_us = std::max((uint32_t) 10, this->value * (this->cycle_time_us - min_us) / 65535 + min_us);
 | 
			
		||||
      this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535);
 | 
			
		||||
    } else {
 | 
			
		||||
      // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic
 | 
			
		||||
      // also take into account min_power
 | 
			
		||||
      auto min_us = this->cycle_time_us * this->min_power / 1000;
 | 
			
		||||
      this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535);
 | 
			
		||||
 | 
			
		||||
      if (this->method == DIM_METHOD_LEADING_PULSE) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import output
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.components import output
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MIN_POWER, CONF_METHOD
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@glmnet"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
from esphome.components.light.effects import register_addressable_effect
 | 
			
		||||
from esphome.components.light.types import AddressableLightEffect
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
from esphome.components.light.types import AddressableLightEffect
 | 
			
		||||
from esphome.components.light.effects import register_addressable_effect
 | 
			
		||||
from esphome.const import CONF_NAME, CONF_UART_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 
 | 
			
		||||
@@ -36,21 +36,12 @@ ATTENUATION_MODES = {
 | 
			
		||||
    "auto": "auto",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
sampling_mode = adc_ns.enum("SamplingMode", is_class=True)
 | 
			
		||||
 | 
			
		||||
SAMPLING_MODES = {
 | 
			
		||||
    "avg": sampling_mode.AVG,
 | 
			
		||||
    "min": sampling_mode.MIN,
 | 
			
		||||
    "max": sampling_mode.MAX,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
adc1_channel_t = cg.global_ns.enum("adc1_channel_t")
 | 
			
		||||
adc2_channel_t = cg.global_ns.enum("adc2_channel_t")
 | 
			
		||||
 | 
			
		||||
# From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h
 | 
			
		||||
# pin to adc1 channel mapping
 | 
			
		||||
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
 | 
			
		||||
ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32: {
 | 
			
		||||
        36: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        37: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -61,41 +52,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        34: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
        35: adc1_channel_t.ADC1_CHANNEL_7,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C6: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_5,
 | 
			
		||||
        6: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32H2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -108,7 +64,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        9: adc1_channel_t.ADC1_CHANNEL_8,
 | 
			
		||||
        10: adc1_channel_t.ADC1_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S3: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
@@ -121,12 +76,40 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
 | 
			
		||||
        9: adc1_channel_t.ADC1_CHANNEL_8,
 | 
			
		||||
        10: adc1_channel_t.ADC1_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C6: {
 | 
			
		||||
        0: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_5,
 | 
			
		||||
        6: adc1_channel_t.ADC1_CHANNEL_6,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32H2: {
 | 
			
		||||
        1: adc1_channel_t.ADC1_CHANNEL_0,
 | 
			
		||||
        2: adc1_channel_t.ADC1_CHANNEL_1,
 | 
			
		||||
        3: adc1_channel_t.ADC1_CHANNEL_2,
 | 
			
		||||
        4: adc1_channel_t.ADC1_CHANNEL_3,
 | 
			
		||||
        5: adc1_channel_t.ADC1_CHANNEL_4,
 | 
			
		||||
    },
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# pin to adc2 channel mapping
 | 
			
		||||
# https://github.com/espressif/esp-idf/blob/v4.4.8/components/driver/include/driver/adc.h
 | 
			
		||||
ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32/include/soc/adc_channel.h
 | 
			
		||||
    # TODO: add other variants
 | 
			
		||||
    VARIANT_ESP32: {
 | 
			
		||||
        4: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        0: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -139,19 +122,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        25: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        26: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C2: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32C6: {},  # no ADC2
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32H2: {},  # no ADC2
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S2: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -164,7 +134,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    # https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s3/include/soc/adc_channel.h
 | 
			
		||||
    VARIANT_ESP32S3: {
 | 
			
		||||
        11: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
        12: adc2_channel_t.ADC2_CHANNEL_1,
 | 
			
		||||
@@ -177,6 +146,12 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
        19: adc2_channel_t.ADC2_CHANNEL_8,
 | 
			
		||||
        20: adc2_channel_t.ADC2_CHANNEL_9,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C2: {},
 | 
			
		||||
    VARIANT_ESP32C6: {},
 | 
			
		||||
    VARIANT_ESP32H2: {},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,21 +28,6 @@ static const adc_atten_t ADC_ATTEN_DB_12_COMPAT = ADC_ATTEN_DB_11;
 | 
			
		||||
#endif
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 | 
			
		||||
enum class SamplingMode : uint8_t { AVG = 0, MIN = 1, MAX = 2 };
 | 
			
		||||
const LogString *sampling_mode_to_str(SamplingMode mode);
 | 
			
		||||
 | 
			
		||||
class Aggregator {
 | 
			
		||||
 public:
 | 
			
		||||
  void add_sample(uint32_t value);
 | 
			
		||||
  uint32_t aggregate();
 | 
			
		||||
  Aggregator(SamplingMode mode);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  SamplingMode mode_{SamplingMode::AVG};
 | 
			
		||||
  uint32_t aggr_{0};
 | 
			
		||||
  uint32_t samples_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler {
 | 
			
		||||
 public:
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
@@ -69,7 +54,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
  void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
 | 
			
		||||
  void set_output_raw(bool output_raw) { this->output_raw_ = output_raw; }
 | 
			
		||||
  void set_sample_count(uint8_t sample_count);
 | 
			
		||||
  void set_sampling_mode(SamplingMode sampling_mode);
 | 
			
		||||
  float sample() override;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
@@ -84,7 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
 | 
			
		||||
  InternalGPIOPin *pin_;
 | 
			
		||||
  bool output_raw_{false};
 | 
			
		||||
  uint8_t sample_count_{1};
 | 
			
		||||
  SamplingMode sampling_mode_{SamplingMode::AVG};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_RP2040
 | 
			
		||||
  bool is_temperature_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -6,59 +6,6 @@ namespace adc {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "adc.common";
 | 
			
		||||
 | 
			
		||||
const LogString *sampling_mode_to_str(SamplingMode mode) {
 | 
			
		||||
  switch (mode) {
 | 
			
		||||
    case SamplingMode::AVG:
 | 
			
		||||
      return LOG_STR("average");
 | 
			
		||||
    case SamplingMode::MIN:
 | 
			
		||||
      return LOG_STR("minimum");
 | 
			
		||||
    case SamplingMode::MAX:
 | 
			
		||||
      return LOG_STR("maximum");
 | 
			
		||||
  }
 | 
			
		||||
  return LOG_STR("unknown");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
Aggregator::Aggregator(SamplingMode mode) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  // set to max uint if mode is "min"
 | 
			
		||||
  if (mode == SamplingMode::MIN) {
 | 
			
		||||
    this->aggr_ = UINT32_MAX;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Aggregator::add_sample(uint32_t value) {
 | 
			
		||||
  this->samples_ += 1;
 | 
			
		||||
 | 
			
		||||
  switch (this->mode_) {
 | 
			
		||||
    case SamplingMode::AVG:
 | 
			
		||||
      this->aggr_ += value;
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case SamplingMode::MIN:
 | 
			
		||||
      if (value < this->aggr_) {
 | 
			
		||||
        this->aggr_ = value;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case SamplingMode::MAX:
 | 
			
		||||
      if (value > this->aggr_) {
 | 
			
		||||
        this->aggr_ = value;
 | 
			
		||||
      }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t Aggregator::aggregate() {
 | 
			
		||||
  if (this->mode_ == SamplingMode::AVG) {
 | 
			
		||||
    if (this->samples_ == 0) {
 | 
			
		||||
      return this->aggr_;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (this->aggr_ + (this->samples_ >> 1)) / this->samples_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return this->aggr_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ADCSensor::update() {
 | 
			
		||||
  float value_v = this->sample();
 | 
			
		||||
  ESP_LOGV(TAG, "'%s': Got voltage=%.4fV", this->get_name().c_str(), value_v);
 | 
			
		||||
@@ -71,8 +18,6 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
 | 
			
		||||
 | 
			
		||||
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
}  // namespace adc
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;
 | 
			
		||||
static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup for '%s'...", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
 | 
			
		||||
 | 
			
		||||
  if (this->channel1_ != ADC1_CHANNEL_MAX) {
 | 
			
		||||
    adc1_config_width(ADC_WIDTH_MAX_SOC_BITS);
 | 
			
		||||
@@ -78,14 +78,12 @@ void ADCSensor::dump_config() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
  if (!this->autorange_) {
 | 
			
		||||
    auto aggr = Aggregator(this->sampling_mode_);
 | 
			
		||||
 | 
			
		||||
    uint32_t sum = 0;
 | 
			
		||||
    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
      int raw = -1;
 | 
			
		||||
      if (this->channel1_ != ADC1_CHANNEL_MAX) {
 | 
			
		||||
@@ -96,14 +94,13 @@ float ADCSensor::sample() {
 | 
			
		||||
      if (raw == -1) {
 | 
			
		||||
        return NAN;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      aggr.add_sample(raw);
 | 
			
		||||
      sum += raw;
 | 
			
		||||
    }
 | 
			
		||||
    sum = (sum + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
    if (this->output_raw_) {
 | 
			
		||||
      return aggr.aggregate();
 | 
			
		||||
      return sum;
 | 
			
		||||
    }
 | 
			
		||||
    uint32_t mv =
 | 
			
		||||
        esp_adc_cal_raw_to_voltage(aggr.aggregate(), &this->cal_characteristics_[(int32_t) this->attenuation_]);
 | 
			
		||||
    uint32_t mv = esp_adc_cal_raw_to_voltage(sum, &this->cal_characteristics_[(int32_t) this->attenuation_]);
 | 
			
		||||
    return mv / 1000.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ namespace adc {
 | 
			
		||||
static const char *const TAG = "adc.esp8266";
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup for '%s'...", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
 | 
			
		||||
#ifndef USE_ADC_SENSOR_VCC
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
#endif
 | 
			
		||||
@@ -31,27 +31,23 @@ void ADCSensor::dump_config() {
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
#endif  // USE_ADC_SENSOR_VCC
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
  auto aggr = Aggregator(this->sampling_mode_);
 | 
			
		||||
 | 
			
		||||
  uint32_t raw = 0;
 | 
			
		||||
  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
    uint32_t raw = 0;
 | 
			
		||||
#ifdef USE_ADC_SENSOR_VCC
 | 
			
		||||
    raw = ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
    raw += ESP.getVcc();  // NOLINT(readability-static-accessed-through-instance)
 | 
			
		||||
#else
 | 
			
		||||
    raw = analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
    raw += analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
#endif  // USE_ADC_SENSOR_VCC
 | 
			
		||||
    aggr.add_sample(raw);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
  if (this->output_raw_) {
 | 
			
		||||
    return aggr.aggregate();
 | 
			
		||||
    return raw;
 | 
			
		||||
  }
 | 
			
		||||
  return aggr.aggregate() / 1024.0f;
 | 
			
		||||
  return raw / 1024.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace adc {
 | 
			
		||||
static const char *const TAG = "adc.libretiny";
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup for '%s'...", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
 | 
			
		||||
#ifndef USE_ADC_SENSOR_VCC
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
#endif  // !USE_ADC_SENSOR_VCC
 | 
			
		||||
@@ -23,28 +23,23 @@ void ADCSensor::dump_config() {
 | 
			
		||||
  LOG_PIN("  Pin: ", this->pin_);
 | 
			
		||||
#endif  // USE_ADC_SENSOR_VCC
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
  uint32_t raw = 0;
 | 
			
		||||
  auto aggr = Aggregator(this->sampling_mode_);
 | 
			
		||||
 | 
			
		||||
  if (this->output_raw_) {
 | 
			
		||||
    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
      raw = analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
      aggr.add_sample(raw);
 | 
			
		||||
      raw += analogRead(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
    }
 | 
			
		||||
    return aggr.aggregate();
 | 
			
		||||
    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
    return raw;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
    raw = analogReadVoltage(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
    aggr.add_sample(raw);
 | 
			
		||||
    raw += analogReadVoltage(this->pin_->get_pin());  // NOLINT
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return aggr.aggregate() / 1000.0f;
 | 
			
		||||
  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
  return raw / 1000.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace adc
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace adc {
 | 
			
		||||
static const char *const TAG = "adc.rp2040";
 | 
			
		||||
 | 
			
		||||
void ADCSensor::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup for '%s'...", this->get_name().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str());
 | 
			
		||||
  static bool initialized = false;
 | 
			
		||||
  if (!initialized) {
 | 
			
		||||
    adc_init();
 | 
			
		||||
@@ -34,28 +34,24 @@ void ADCSensor::dump_config() {
 | 
			
		||||
#endif  // USE_ADC_SENSOR_VCC
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Samples: %i", this->sample_count_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Sampling mode: %s", LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ADCSensor::sample() {
 | 
			
		||||
  uint32_t raw = 0;
 | 
			
		||||
  auto aggr = Aggregator(this->sampling_mode_);
 | 
			
		||||
 | 
			
		||||
  if (this->is_temperature_) {
 | 
			
		||||
    adc_set_temp_sensor_enabled(true);
 | 
			
		||||
    delay(1);
 | 
			
		||||
    adc_select_input(4);
 | 
			
		||||
 | 
			
		||||
    uint32_t raw = 0;
 | 
			
		||||
    for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
      raw = adc_read();
 | 
			
		||||
      aggr.add_sample(raw);
 | 
			
		||||
      raw += adc_read();
 | 
			
		||||
    }
 | 
			
		||||
    raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
    adc_set_temp_sensor_enabled(false);
 | 
			
		||||
    if (this->output_raw_) {
 | 
			
		||||
      return aggr.aggregate();
 | 
			
		||||
      return raw;
 | 
			
		||||
    }
 | 
			
		||||
    return aggr.aggregate() * 3.3f / 4096.0f;
 | 
			
		||||
    return raw * 3.3f / 4096.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t pin = this->pin_->get_pin();
 | 
			
		||||
@@ -72,10 +68,11 @@ float ADCSensor::sample() {
 | 
			
		||||
  adc_gpio_init(pin);
 | 
			
		||||
  adc_select_input(pin - 26);
 | 
			
		||||
 | 
			
		||||
  uint32_t raw = 0;
 | 
			
		||||
  for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
 | 
			
		||||
    raw = adc_read();
 | 
			
		||||
    aggr.add_sample(raw);
 | 
			
		||||
    raw += adc_read();
 | 
			
		||||
  }
 | 
			
		||||
  raw = (raw + (this->sample_count_ >> 1)) / this->sample_count_;  // NOLINT(clang-analyzer-core.DivideZero)
 | 
			
		||||
 | 
			
		||||
#ifdef CYW43_USES_VSYS_PIN
 | 
			
		||||
  if (pin == PICO_VSYS_PIN) {
 | 
			
		||||
@@ -84,10 +81,10 @@ float ADCSensor::sample() {
 | 
			
		||||
#endif  // CYW43_USES_VSYS_PIN
 | 
			
		||||
 | 
			
		||||
  if (this->output_raw_) {
 | 
			
		||||
    return aggr.aggregate();
 | 
			
		||||
    return raw;
 | 
			
		||||
  }
 | 
			
		||||
  float coeff = pin == PICO_VSYS_PIN ? 3.0f : 1.0f;
 | 
			
		||||
  return aggr.aggregate() * 3.3f / 4096.0f * coeff;
 | 
			
		||||
  return raw * 3.3f / 4096.0f * coeff;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace adc
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.components.esp32 import get_esp32_variant
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ATTENUATION,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
@@ -15,14 +17,10 @@ from esphome.const import (
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
from . import (
 | 
			
		||||
    ATTENUATION_MODES,
 | 
			
		||||
    ESP32_VARIANT_ADC1_PIN_TO_CHANNEL,
 | 
			
		||||
    ESP32_VARIANT_ADC2_PIN_TO_CHANNEL,
 | 
			
		||||
    SAMPLING_MODES,
 | 
			
		||||
    adc_ns,
 | 
			
		||||
    validate_adc_pin,
 | 
			
		||||
)
 | 
			
		||||
@@ -32,11 +30,9 @@ _LOGGER = logging.getLogger(__name__)
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
 | 
			
		||||
CONF_SAMPLES = "samples"
 | 
			
		||||
CONF_SAMPLING_MODE = "sampling_mode"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
_attenuation = cv.enum(ATTENUATION_MODES, lower=True)
 | 
			
		||||
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_config(config):
 | 
			
		||||
@@ -92,7 +88,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                cv.only_on_esp32, _attenuation
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
 | 
			
		||||
            cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s")),
 | 
			
		||||
@@ -117,7 +112,6 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_output_raw(config[CONF_RAW]))
 | 
			
		||||
    cg.add(var.set_sample_count(config[CONF_SAMPLES]))
 | 
			
		||||
    cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
 | 
			
		||||
 | 
			
		||||
    if attenuation := config.get(CONF_ATTENUATION):
 | 
			
		||||
        if attenuation == "auto":
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import spi
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ static const char *const TAG = "adc128s102";
 | 
			
		||||
float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
void ADC128S102::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up adc128s102");
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_CHANNEL, CONF_ID
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.const import CONF_ID, CONF_CHANNEL
 | 
			
		||||
 | 
			
		||||
from .. import ADC128S102, adc128s102_ns
 | 
			
		||||
from .. import adc128s102_ns, ADC128S102
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
DEPENDENCIES = ["adc128s102"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,15 +1,15 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import display, light
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import display, light
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ADDRESSABLE_LIGHT_ID,
 | 
			
		||||
    CONF_HEIGHT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
    CONF_PIXEL_MAPPER,
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
    CONF_ADDRESSABLE_LIGHT_ID,
 | 
			
		||||
    CONF_HEIGHT,
 | 
			
		||||
    CONF_WIDTH,
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
    CONF_PIXEL_MAPPER,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@justfalter"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, i2c
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ACTIVE_POWER,
 | 
			
		||||
    CONF_APPARENT_POWER,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,27 +1,27 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
    CONF_IRQ_PIN,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    CONF_FREQUENCY,
 | 
			
		||||
    CONF_VOLTAGE_GAIN,
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_POWER,
 | 
			
		||||
    DEVICE_CLASS_POWER_FACTOR,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
    DEVICE_CLASS_POWER_FACTOR,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_VOLT_AMPS,
 | 
			
		||||
    UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_VOLT_AMPS_REACTIVE,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_CURRENT_A = "current_a"
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ade7953_base, i2c
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, ade7953_base
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
AUTO_LOAD = ["ade7953_base"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ade7953_base, spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import spi, ade7953_base
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
AUTO_LOAD = ["ade7953_base"]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 
 | 
			
		||||
@@ -9,14 +9,18 @@ static const char *const TAG = "ads1115";
 | 
			
		||||
static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00;
 | 
			
		||||
static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
 | 
			
		||||
 | 
			
		||||
static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;  // 3300_SPS for ADS1015
 | 
			
		||||
 | 
			
		||||
void ADS1115Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
 | 
			
		||||
  uint16_t value;
 | 
			
		||||
  if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Configuring ADS1115...");
 | 
			
		||||
 | 
			
		||||
  uint16_t config = 0;
 | 
			
		||||
  // Clear single-shot bit
 | 
			
		||||
  //        0b0xxxxxxxxxxxxxxx
 | 
			
		||||
@@ -39,9 +43,9 @@ void ADS1115Component::setup() {
 | 
			
		||||
    config |= 0b0000000100000000;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set data rate - 860 samples per second
 | 
			
		||||
  // Set data rate - 860 samples per second (we're in singleshot mode)
 | 
			
		||||
  //        0bxxxxxxxx100xxxxx
 | 
			
		||||
  config |= ADS1115_860SPS << 5;
 | 
			
		||||
  config |= ADS1115_DATA_RATE_860_SPS << 5;
 | 
			
		||||
 | 
			
		||||
  // Set comparator mode - hysteresis
 | 
			
		||||
  //        0bxxxxxxxxxxx0xxxx
 | 
			
		||||
@@ -66,14 +70,14 @@ void ADS1115Component::setup() {
 | 
			
		||||
  this->prev_config_ = config;
 | 
			
		||||
}
 | 
			
		||||
void ADS1115Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ADS1115:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ADS1115...");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with ADS1115 failed!");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain,
 | 
			
		||||
                                            ADS1115Resolution resolution, ADS1115Samplerate samplerate) {
 | 
			
		||||
                                            ADS1115Resolution resolution) {
 | 
			
		||||
  uint16_t config = this->prev_config_;
 | 
			
		||||
  // Multiplexer
 | 
			
		||||
  //        0bxBBBxxxxxxxxxxxx
 | 
			
		||||
@@ -85,11 +89,6 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
 | 
			
		||||
  config &= 0b1111000111111111;
 | 
			
		||||
  config |= (gain & 0b111) << 9;
 | 
			
		||||
 | 
			
		||||
  // Sample rate
 | 
			
		||||
  //        0bxxxxxxxxBBBxxxxx
 | 
			
		||||
  config &= 0b1111111100011111;
 | 
			
		||||
  config |= (samplerate & 0b111) << 5;
 | 
			
		||||
 | 
			
		||||
  if (!this->continuous_mode_) {
 | 
			
		||||
    // Start conversion
 | 
			
		||||
    config |= 0b1000000000000000;
 | 
			
		||||
@@ -102,54 +101,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
 | 
			
		||||
    }
 | 
			
		||||
    this->prev_config_ = config;
 | 
			
		||||
 | 
			
		||||
    // Delay calculated as: ceil((1000/SPS)+.5)
 | 
			
		||||
    if (resolution == ADS1015_12_BITS) {
 | 
			
		||||
      switch (samplerate) {
 | 
			
		||||
        case ADS1115_8SPS:
 | 
			
		||||
          delay(9);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_16SPS:
 | 
			
		||||
          delay(5);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_32SPS:
 | 
			
		||||
          delay(3);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_64SPS:
 | 
			
		||||
        case ADS1115_128SPS:
 | 
			
		||||
          delay(2);
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          delay(1);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      switch (samplerate) {
 | 
			
		||||
        case ADS1115_8SPS:
 | 
			
		||||
          delay(126);  // NOLINT
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_16SPS:
 | 
			
		||||
          delay(63);  // NOLINT
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_32SPS:
 | 
			
		||||
          delay(32);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_64SPS:
 | 
			
		||||
          delay(17);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_128SPS:
 | 
			
		||||
          delay(9);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_250SPS:
 | 
			
		||||
          delay(5);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_475SPS:
 | 
			
		||||
          delay(3);
 | 
			
		||||
          break;
 | 
			
		||||
        case ADS1115_860SPS:
 | 
			
		||||
          delay(2);
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // about 1.2 ms with 860 samples per second
 | 
			
		||||
    delay(2);
 | 
			
		||||
 | 
			
		||||
    // in continuous mode, conversion will always be running, rely on the delay
 | 
			
		||||
    // to ensure conversion is taking place with the correct settings
 | 
			
		||||
 
 | 
			
		||||
@@ -33,17 +33,6 @@ enum ADS1115Resolution {
 | 
			
		||||
  ADS1015_12_BITS = 12,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum ADS1115Samplerate {
 | 
			
		||||
  ADS1115_8SPS = 0b000,
 | 
			
		||||
  ADS1115_16SPS = 0b001,
 | 
			
		||||
  ADS1115_32SPS = 0b010,
 | 
			
		||||
  ADS1115_64SPS = 0b011,
 | 
			
		||||
  ADS1115_128SPS = 0b100,
 | 
			
		||||
  ADS1115_250SPS = 0b101,
 | 
			
		||||
  ADS1115_475SPS = 0b110,
 | 
			
		||||
  ADS1115_860SPS = 0b111
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ADS1115Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -53,8 +42,7 @@ class ADS1115Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
  void set_continuous_mode(bool continuous_mode) { continuous_mode_ = continuous_mode; }
 | 
			
		||||
 | 
			
		||||
  /// Helper method to request a measurement from a sensor.
 | 
			
		||||
  float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution,
 | 
			
		||||
                            ADS1115Samplerate samplerate);
 | 
			
		||||
  float request_measurement(ADS1115Multiplexer multiplexer, ADS1115Gain gain, ADS1115Resolution resolution);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint16_t prev_config_{0};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,16 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MULTIPLEXER,
 | 
			
		||||
    CONF_RESOLUTION,
 | 
			
		||||
    CONF_SAMPLE_RATE,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_ns
 | 
			
		||||
from .. import ads1115_ns, ADS1115Component, CONF_ADS1115_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
DEPENDENCIES = ["ads1115"]
 | 
			
		||||
@@ -45,17 +43,6 @@ RESOLUTION = {
 | 
			
		||||
    "12_BITS": ADS1115Resolution.ADS1015_12_BITS,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ADS1115Samplerate = ads1115_ns.enum("ADS1115Samplerate")
 | 
			
		||||
SAMPLERATE = {
 | 
			
		||||
    "8": ADS1115Samplerate.ADS1115_8SPS,
 | 
			
		||||
    "16": ADS1115Samplerate.ADS1115_16SPS,
 | 
			
		||||
    "32": ADS1115Samplerate.ADS1115_32SPS,
 | 
			
		||||
    "64": ADS1115Samplerate.ADS1115_64SPS,
 | 
			
		||||
    "128": ADS1115Samplerate.ADS1115_128SPS,
 | 
			
		||||
    "250": ADS1115Samplerate.ADS1115_250SPS,
 | 
			
		||||
    "475": ADS1115Samplerate.ADS1115_475SPS,
 | 
			
		||||
    "860": ADS1115Samplerate.ADS1115_860SPS,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ADS1115Sensor = ads1115_ns.class_(
 | 
			
		||||
    "ADS1115Sensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
 | 
			
		||||
@@ -77,9 +64,6 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum(
 | 
			
		||||
                RESOLUTION, upper=True, space="_"
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SAMPLE_RATE, default="860"): cv.enum(
 | 
			
		||||
                SAMPLERATE, string=True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
@@ -95,4 +79,3 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER]))
 | 
			
		||||
    cg.add(var.set_gain(config[CONF_GAIN]))
 | 
			
		||||
    cg.add(var.set_resolution(config[CONF_RESOLUTION]))
 | 
			
		||||
    cg.add(var.set_samplerate(config[CONF_SAMPLE_RATE]))
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace ads1115 {
 | 
			
		||||
static const char *const TAG = "ads1115.sensor";
 | 
			
		||||
 | 
			
		||||
float ADS1115Sensor::sample() {
 | 
			
		||||
  return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
 | 
			
		||||
  return this->parent_->request_measurement(this->multiplexer_, this->gain_, this->resolution_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ADS1115Sensor::update() {
 | 
			
		||||
@@ -24,7 +24,6 @@ void ADS1115Sensor::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Multiplexer: %u", this->multiplexer_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Gain: %u", this->gain_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Resolution: %u", this->resolution_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Sample rate: %u", this->samplerate_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ads1115
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,6 @@ class ADS1115Sensor : public sensor::Sensor,
 | 
			
		||||
  void set_multiplexer(ADS1115Multiplexer multiplexer) { this->multiplexer_ = multiplexer; }
 | 
			
		||||
  void set_gain(ADS1115Gain gain) { this->gain_ = gain; }
 | 
			
		||||
  void set_resolution(ADS1115Resolution resolution) { this->resolution_ = resolution; }
 | 
			
		||||
  void set_samplerate(ADS1115Samplerate samplerate) { this->samplerate_ = samplerate; }
 | 
			
		||||
  float sample() override;
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -30,7 +29,6 @@ class ADS1115Sensor : public sensor::Sensor,
 | 
			
		||||
  ADS1115Multiplexer multiplexer_;
 | 
			
		||||
  ADS1115Gain gain_;
 | 
			
		||||
  ADS1115Resolution resolution_;
 | 
			
		||||
  ADS1115Samplerate samplerate_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ads1115
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import spi
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@solomondg1"]
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ static const char *const TAG = "ads1118";
 | 
			
		||||
static const uint8_t ADS1118_DATA_RATE_860_SPS = 0b111;
 | 
			
		||||
 | 
			
		||||
void ADS1118::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ads1118");
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
 | 
			
		||||
  this->config_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,17 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, voltage_sampler
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    CONF_MULTIPLEXER,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .. import ADS1118, CONF_ADS1118_ID, ads1118_ns
 | 
			
		||||
from .. import ads1118_ns, ADS1118, CONF_ADS1118_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["voltage_sampler"]
 | 
			
		||||
DEPENDENCIES = ["ads1118"]
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ static const uint16_t ZP_CURRENT = 0x0000;
 | 
			
		||||
static const uint16_t ZP_DEFAULT = 0xFFFF;
 | 
			
		||||
 | 
			
		||||
void AGS10Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up ags10...");
 | 
			
		||||
 | 
			
		||||
  auto version = this->read_version_();
 | 
			
		||||
  if (version) {
 | 
			
		||||
@@ -65,7 +65,7 @@ void AGS10Component::dump_config() {
 | 
			
		||||
    case NONE:
 | 
			
		||||
      break;
 | 
			
		||||
    case COMMUNICATION_FAILED:
 | 
			
		||||
      ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
      ESP_LOGE(TAG, "Communication with AGS10 failed!");
 | 
			
		||||
      break;
 | 
			
		||||
    case CRC_CHECK_FAILED:
 | 
			
		||||
      ESP_LOGE(TAG, "The crc check failed");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,21 @@
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ADDRESS,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_VALUE,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    ICON_RADIATOR,
 | 
			
		||||
    ICON_RESTART,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_OHM,
 | 
			
		||||
    UNIT_PARTS_PER_BILLION,
 | 
			
		||||
    CONF_ADDRESS,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    CONF_VERSION,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_VALUE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_RESISTANCE = "resistance"
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@
 | 
			
		||||
#include "aht10.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace aht10 {
 | 
			
		||||
@@ -35,59 +34,57 @@ static const uint8_t AHT10_INIT_ATTEMPTS = 10;
 | 
			
		||||
 | 
			
		||||
static const uint8_t AHT10_STATUS_BUSY = 0x80;
 | 
			
		||||
 | 
			
		||||
static const float AHT10_DIVISOR = 1048576.0f;  // 2^20, used for temperature and humidity calculations
 | 
			
		||||
 | 
			
		||||
void AHT10Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
 | 
			
		||||
  if (this->write(AHT10_SOFTRESET_CMD, sizeof(AHT10_SOFTRESET_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Reset failed");
 | 
			
		||||
    ESP_LOGE(TAG, "Reset AHT10 failed!");
 | 
			
		||||
  }
 | 
			
		||||
  delay(AHT10_SOFTRESET_DELAY);
 | 
			
		||||
 | 
			
		||||
  i2c::ErrorCode error_code = i2c::ERROR_INVALID_ARGUMENT;
 | 
			
		||||
  switch (this->variant_) {
 | 
			
		||||
    case AHT10Variant::AHT20:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "Setting up AHT20");
 | 
			
		||||
      error_code = this->write(AHT20_INITIALIZE_CMD, sizeof(AHT20_INITIALIZE_CMD));
 | 
			
		||||
      break;
 | 
			
		||||
    case AHT10Variant::AHT10:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "Setting up AHT10");
 | 
			
		||||
      error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  if (error_code != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t cal_attempts = 0;
 | 
			
		||||
  uint8_t data = AHT10_STATUS_BUSY;
 | 
			
		||||
  int cal_attempts = 0;
 | 
			
		||||
  while (data & AHT10_STATUS_BUSY) {
 | 
			
		||||
    delay(AHT10_DEFAULT_DELAY);
 | 
			
		||||
    if (this->read(&data, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
      ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    ++cal_attempts;
 | 
			
		||||
    if (cal_attempts > AHT10_INIT_ATTEMPTS) {
 | 
			
		||||
      ESP_LOGE(TAG, "Initialization timed out");
 | 
			
		||||
      ESP_LOGE(TAG, "AHT10 initialization timed out!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if ((data & 0x68) != 0x08) {  // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
 | 
			
		||||
    ESP_LOGE(TAG, "Initialization failed");
 | 
			
		||||
    ESP_LOGE(TAG, "AHT10 initialization failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Initialization complete");
 | 
			
		||||
  ESP_LOGV(TAG, "AHT10 initialization");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AHT10Component::restart_read_() {
 | 
			
		||||
  if (this->read_count_ == AHT10_ATTEMPTS) {
 | 
			
		||||
    this->read_count_ = 0;
 | 
			
		||||
    this->status_set_error("Reading timed out");
 | 
			
		||||
    this->status_set_error("Measurements reading timed-out!");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->read_count_++;
 | 
			
		||||
@@ -100,24 +97,24 @@ void AHT10Component::read_data_() {
 | 
			
		||||
    ESP_LOGD(TAG, "Read attempt %d at %ums", this->read_count_, (unsigned) (millis() - this->start_time_));
 | 
			
		||||
  }
 | 
			
		||||
  if (this->read(data, 6) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning("Read failed, will retry");
 | 
			
		||||
    this->status_set_warning("AHT10 read failed, retrying soon");
 | 
			
		||||
    this->restart_read_();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy
 | 
			
		||||
    ESP_LOGD(TAG, "Device busy, will retry");
 | 
			
		||||
    ESP_LOGD(TAG, "AHT10 is busy, waiting...");
 | 
			
		||||
    this->restart_read_();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) {
 | 
			
		||||
    // Invalid humidity (0x0)
 | 
			
		||||
    // Unrealistic humidity (0x0)
 | 
			
		||||
    if (this->humidity_sensor_ == nullptr) {
 | 
			
		||||
      ESP_LOGV(TAG, "Invalid humidity (reading not required)");
 | 
			
		||||
      ESP_LOGV(TAG, "ATH10 Unrealistic humidity (0x0), but humidity is not required");
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "Invalid humidity, retrying...");
 | 
			
		||||
      ESP_LOGD(TAG, "ATH10 Unrealistic humidity (0x0), retrying...");
 | 
			
		||||
      if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
        this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
        this->status_set_warning("Communication with AHT10 failed!");
 | 
			
		||||
      }
 | 
			
		||||
      this->restart_read_();
 | 
			
		||||
      return;
 | 
			
		||||
@@ -126,17 +123,22 @@ void AHT10Component::read_data_() {
 | 
			
		||||
  if (this->read_count_ > 1) {
 | 
			
		||||
    ESP_LOGD(TAG, "Success at %ums", (unsigned) (millis() - this->start_time_));
 | 
			
		||||
  }
 | 
			
		||||
  uint32_t raw_temperature = encode_uint24(data[3] & 0xF, data[4], data[5]);
 | 
			
		||||
  uint32_t raw_humidity = encode_uint24(data[1], data[2], data[3]) >> 4;
 | 
			
		||||
  uint32_t raw_temperature = ((data[3] & 0x0F) << 16) | (data[4] << 8) | data[5];
 | 
			
		||||
  uint32_t raw_humidity = ((data[1] << 16) | (data[2] << 8) | data[3]) >> 4;
 | 
			
		||||
 | 
			
		||||
  if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
    float temperature = ((200.0f * static_cast<float>(raw_temperature)) / AHT10_DIVISOR) - 50.0f;
 | 
			
		||||
    float temperature = ((200.0f * (float) raw_temperature) / 1048576.0f) - 50.0f;
 | 
			
		||||
    this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    float humidity = raw_humidity == 0 ? NAN : static_cast<float>(raw_humidity) * 100.0f / AHT10_DIVISOR;
 | 
			
		||||
    float humidity;
 | 
			
		||||
    if (raw_humidity == 0) {  // unrealistic value
 | 
			
		||||
      humidity = NAN;
 | 
			
		||||
    } else {
 | 
			
		||||
      humidity = (float) raw_humidity * 100.0f / 1048576.0f;
 | 
			
		||||
    }
 | 
			
		||||
    if (std::isnan(humidity)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid humidity reading (0%%), ");
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum");
 | 
			
		||||
    }
 | 
			
		||||
    this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
  }
 | 
			
		||||
@@ -148,7 +150,7 @@ void AHT10Component::update() {
 | 
			
		||||
    return;
 | 
			
		||||
  this->start_time_ = millis();
 | 
			
		||||
  if (this->write(AHT10_MEASURE_CMD, sizeof(AHT10_MEASURE_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    this->status_set_warning("Communication with AHT10 failed!");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->restart_read_();
 | 
			
		||||
@@ -160,7 +162,7 @@ void AHT10Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "AHT10:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AHT10 failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    CONF_VARIANT,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    CONF_VARIANT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ static const char *const TAG = "aic3204";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
void AIC3204::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AIC3204...");
 | 
			
		||||
 | 
			
		||||
  // Set register page to 0
 | 
			
		||||
  ERROR_CHECK(this->write_byte(AIC3204_PAGE_CTRL, 0x00), "Set page 0 failed");
 | 
			
		||||
@@ -113,7 +113,7 @@ void AIC3204::dump_config() {
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AIC3204 failed");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32_ble_tracker
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import esp32_ble_tracker
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,18 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BATTERY_VOLTAGE,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    CONF_TVOC,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS,
 | 
			
		||||
    DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
@@ -34,7 +35,7 @@ AirthingsWaveBase = airthings_wave_base_ns.class_(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BASE_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
    sensor.SENSOR_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import airthings_wave_base
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from esphome.components import airthings_wave_base
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,20 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import airthings_wave_base, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, airthings_wave_base
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CO2,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ICON_RADIOACTIVE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    CONF_RADON,
 | 
			
		||||
    CONF_RADON_LONG_TERM,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
    ICON_RADIOACTIVE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    CONF_CO2,
 | 
			
		||||
    UNIT_BECQUEREL_PER_CUBIC_METER,
 | 
			
		||||
    UNIT_LUX,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    UNIT_LUX,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
 | 
			
		||||
 
 | 
			
		||||
@@ -5,8 +5,6 @@ from esphome.components import mqtt, web_server
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CODE,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MQTT_ID,
 | 
			
		||||
    CONF_ON_STATE,
 | 
			
		||||
@@ -14,7 +12,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_WEB_SERVER,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
 | 
			
		||||
@@ -81,11 +78,12 @@ AlarmControlPanelCondition = alarm_control_panel_ns.class_(
 | 
			
		||||
    "AlarmControlPanelCondition", automation.Condition
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
_ALARM_CONTROL_PANEL_SCHEMA = (
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA = (
 | 
			
		||||
    cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
 | 
			
		||||
    .extend(cv.MQTT_COMMAND_COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AlarmControlPanel),
 | 
			
		||||
            cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
 | 
			
		||||
                mqtt.MQTTAlarmControlPanelComponent
 | 
			
		||||
            ),
 | 
			
		||||
@@ -148,33 +146,6 @@ _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(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(AlarmControlPanel),
 | 
			
		||||
@@ -238,12 +209,6 @@ async def register_alarm_control_panel(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(
 | 
			
		||||
    "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +1,20 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_CURRENT,
 | 
			
		||||
    CONF_FLOW,
 | 
			
		||||
    CONF_HEAD,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_POWER,
 | 
			
		||||
    CONF_SPEED,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    UNIT_AMPERE,
 | 
			
		||||
    UNIT_CUBIC_METER_PER_HOUR,
 | 
			
		||||
    UNIT_METER,
 | 
			
		||||
    UNIT_REVOLUTIONS_PER_MINUTE,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_METER,
 | 
			
		||||
    UNIT_CUBIC_METER_PER_HOUR,
 | 
			
		||||
    UNIT_REVOLUTIONS_PER_MINUTE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
alpha3_ns = cg.esphome_ns.namespace("alpha3")
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AM2315C::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AM2315C...");
 | 
			
		||||
 | 
			
		||||
  // get status
 | 
			
		||||
  uint8_t status = 0;
 | 
			
		||||
@@ -128,7 +128,7 @@ void AM2315C::update() {
 | 
			
		||||
  data[2] = 0x00;
 | 
			
		||||
  if (this->write(data, 3) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Write failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -138,12 +138,12 @@ void AM2315C::update() {
 | 
			
		||||
    uint8_t status = 0;
 | 
			
		||||
    if (this->read(&status, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Read failed!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if ((status & 0x80) == 0x80) {
 | 
			
		||||
      ESP_LOGE(TAG, "HW still busy!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -151,7 +151,7 @@ void AM2315C::update() {
 | 
			
		||||
    uint8_t data[7];
 | 
			
		||||
    if (this->read(data, 7) != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Read failed!");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -188,7 +188,7 @@ void AM2315C::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "AM2315C:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AM2315C failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@ void AM2320Component::update() {
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
void AM2320Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AM2320...");
 | 
			
		||||
  uint8_t data[8];
 | 
			
		||||
  data[0] = 0;
 | 
			
		||||
  data[1] = 4;
 | 
			
		||||
@@ -47,7 +47,7 @@ void AM2320Component::dump_config() {
 | 
			
		||||
  ESP_LOGD(TAG, "AM2320:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AM2320 failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, cover
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_PIN
 | 
			
		||||
from esphome.components import cover, ble_client
 | 
			
		||||
from esphome.const import CONF_ID, CONF_PIN
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
DEPENDENCIES = ["ble_client"]
 | 
			
		||||
@@ -15,9 +15,9 @@ Am43Component = am43_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cover.cover_schema(Am43Component)
 | 
			
		||||
    .extend(
 | 
			
		||||
    cover.COVER_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Am43Component),
 | 
			
		||||
            cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF),
 | 
			
		||||
            cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean,
 | 
			
		||||
        }
 | 
			
		||||
@@ -28,8 +28,9 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await cover.new_cover(config)
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_pin(config[CONF_PIN]))
 | 
			
		||||
    cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await cover.register_cover(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,12 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor, ble_client
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BATTERY_LEVEL,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    CONF_BATTERY_LEVEL,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    CONF_ILLUMINANCE,
 | 
			
		||||
    ICON_BRIGHTNESS_5,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -14,8 +14,7 @@ void AnalogThresholdBinarySensor::setup() {
 | 
			
		||||
  if (std::isnan(sensor_value)) {
 | 
			
		||||
    this->publish_initial_state(false);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->publish_initial_state(sensor_value >=
 | 
			
		||||
                                (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
 | 
			
		||||
    this->publish_initial_state(sensor_value >= (this->lower_threshold_ + this->upper_threshold_) / 2.0f);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -25,8 +24,7 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
 | 
			
		||||
  this->sensor_->add_on_state_callback([this](float sensor_value) {
 | 
			
		||||
    // if there is an invalid sensor reading, ignore the change and keep the current state
 | 
			
		||||
    if (!std::isnan(sensor_value)) {
 | 
			
		||||
      this->publish_state(sensor_value >=
 | 
			
		||||
                          (this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
 | 
			
		||||
      this->publish_state(sensor_value >= (this->state ? this->lower_threshold_ : this->upper_threshold_));
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -34,8 +32,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
 | 
			
		||||
void AnalogThresholdBinarySensor::dump_config() {
 | 
			
		||||
  LOG_BINARY_SENSOR("", "Analog Threshold Binary Sensor", this);
 | 
			
		||||
  LOG_SENSOR("  ", "Sensor", this->sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_.value());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_.value());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Upper threshold: %.11f", this->upper_threshold_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Lower threshold: %.11f", this->lower_threshold_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
 
 | 
			
		||||
@@ -15,13 +15,14 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *analog_sensor);
 | 
			
		||||
  template<typename T> void set_upper_threshold(T upper_threshold) { this->upper_threshold_ = upper_threshold; }
 | 
			
		||||
  template<typename T> void set_lower_threshold(T lower_threshold) { this->lower_threshold_ = lower_threshold; }
 | 
			
		||||
  void set_upper_threshold(float threshold) { this->upper_threshold_ = threshold; }
 | 
			
		||||
  void set_lower_threshold(float threshold) { this->lower_threshold_ = threshold; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *sensor_{nullptr};
 | 
			
		||||
  TemplatableValue<float> upper_threshold_{};
 | 
			
		||||
  TemplatableValue<float> lower_threshold_{};
 | 
			
		||||
 | 
			
		||||
  float upper_threshold_;
 | 
			
		||||
  float lower_threshold_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace analog_threshold
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,10 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_SENSOR_ID, CONF_THRESHOLD
 | 
			
		||||
from esphome.components import binary_sensor, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_SENSOR_ID,
 | 
			
		||||
    CONF_THRESHOLD,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold")
 | 
			
		||||
 | 
			
		||||
@@ -18,11 +21,11 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_SENSOR_ID): cv.use_id(sensor.Sensor),
 | 
			
		||||
            cv.Required(CONF_THRESHOLD): cv.Any(
 | 
			
		||||
                cv.templatable(cv.float_),
 | 
			
		||||
                cv.float_,
 | 
			
		||||
                cv.Schema(
 | 
			
		||||
                    {
 | 
			
		||||
                        cv.Required(CONF_UPPER): cv.templatable(cv.float_),
 | 
			
		||||
                        cv.Required(CONF_LOWER): cv.templatable(cv.float_),
 | 
			
		||||
                        cv.Required(CONF_UPPER): cv.float_,
 | 
			
		||||
                        cv.Required(CONF_LOWER): cv.float_,
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
            ),
 | 
			
		||||
@@ -39,11 +42,9 @@ async def to_code(config):
 | 
			
		||||
    sens = await cg.get_variable(config[CONF_SENSOR_ID])
 | 
			
		||||
    cg.add(var.set_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if isinstance(config[CONF_THRESHOLD], dict):
 | 
			
		||||
        lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
 | 
			
		||||
        upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
 | 
			
		||||
    if isinstance(config[CONF_THRESHOLD], float):
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD]))
 | 
			
		||||
    else:
 | 
			
		||||
        lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
 | 
			
		||||
        upper = lower
 | 
			
		||||
    cg.add(var.set_upper_threshold(upper))
 | 
			
		||||
    cg.add(var.set_lower_threshold(lower))
 | 
			
		||||
        cg.add(var.set_upper_threshold(config[CONF_THRESHOLD][CONF_UPPER]))
 | 
			
		||||
        cg.add(var.set_lower_threshold(config[CONF_THRESHOLD][CONF_LOWER]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,28 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome import automation, core
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.components.image as espImage
 | 
			
		||||
from esphome.components.image import (
 | 
			
		||||
    CONF_USE_TRANSPARENCY,
 | 
			
		||||
    LOCAL_SCHEMA,
 | 
			
		||||
    SOURCE_LOCAL,
 | 
			
		||||
    SOURCE_WEB,
 | 
			
		||||
    WEB_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_REPEAT
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FILE,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_PATH,
 | 
			
		||||
    CONF_RAW_DATA_ID,
 | 
			
		||||
    CONF_REPEAT,
 | 
			
		||||
    CONF_RESIZE,
 | 
			
		||||
    CONF_SOURCE,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    CONF_URL,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, HexInt
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -12,7 +30,6 @@ AUTO_LOAD = ["image"]
 | 
			
		||||
CODEOWNERS = ["@syndlex"]
 | 
			
		||||
DEPENDENCIES = ["display"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
MULTI_CONF_NO_DEFAULT = True
 | 
			
		||||
 | 
			
		||||
CONF_LOOP = "loop"
 | 
			
		||||
CONF_START_FRAME = "start_frame"
 | 
			
		||||
@@ -34,20 +51,87 @@ SetFrameAction = animation_ns.class_(
 | 
			
		||||
    "AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
 | 
			
		||||
TYPED_FILE_SCHEMA = cv.typed_schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_ID): cv.declare_id(Animation_),
 | 
			
		||||
        cv.Optional(CONF_LOOP): cv.All(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
 | 
			
		||||
                cv.Optional(CONF_END_FRAME): cv.positive_int,
 | 
			
		||||
                cv.Optional(CONF_REPEAT): cv.positive_int,
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        SOURCE_LOCAL: LOCAL_SCHEMA,
 | 
			
		||||
        SOURCE_WEB: WEB_SCHEMA,
 | 
			
		||||
    },
 | 
			
		||||
    key=CONF_SOURCE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _file_schema(value):
 | 
			
		||||
    if isinstance(value, str):
 | 
			
		||||
        return validate_file_shorthand(value)
 | 
			
		||||
    return TYPED_FILE_SCHEMA(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FILE_SCHEMA = cv.Schema(_file_schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_file_shorthand(value):
 | 
			
		||||
    value = cv.string_strict(value)
 | 
			
		||||
    if value.startswith("http://") or value.startswith("https://"):
 | 
			
		||||
        return FILE_SCHEMA(
 | 
			
		||||
            {
 | 
			
		||||
                CONF_SOURCE: SOURCE_WEB,
 | 
			
		||||
                CONF_URL: value,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
    return FILE_SCHEMA(
 | 
			
		||||
        {
 | 
			
		||||
            CONF_SOURCE: SOURCE_LOCAL,
 | 
			
		||||
            CONF_PATH: value,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_cross_dependencies(config):
 | 
			
		||||
    """
 | 
			
		||||
    Validate fields whose possible values depend on other fields.
 | 
			
		||||
    For example, validate that explicitly transparent image types
 | 
			
		||||
    have "use_transparency" set to True.
 | 
			
		||||
    Also set the default value for those kind of dependent fields.
 | 
			
		||||
    """
 | 
			
		||||
    image_type = config[CONF_TYPE]
 | 
			
		||||
    is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"]
 | 
			
		||||
    # If the use_transparency option was not specified, set the default depending on the image type
 | 
			
		||||
    if CONF_USE_TRANSPARENCY not in config:
 | 
			
		||||
        config[CONF_USE_TRANSPARENCY] = is_transparent_type
 | 
			
		||||
 | 
			
		||||
    if is_transparent_type and not config[CONF_USE_TRANSPARENCY]:
 | 
			
		||||
        raise cv.Invalid(f"Image type {image_type} must always be transparent.")
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ANIMATION_SCHEMA = cv.Schema(
 | 
			
		||||
    cv.All(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.declare_id(Animation_),
 | 
			
		||||
            cv.Required(CONF_FILE): FILE_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_RESIZE): cv.dimensions,
 | 
			
		||||
            cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(
 | 
			
		||||
                espImage.IMAGE_TYPE, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            # Not setting default here on purpose; the default depends on the image type,
 | 
			
		||||
            # and thus will be set in the "validate_cross_dependencies" validator.
 | 
			
		||||
            cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_LOOP): cv.All(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
 | 
			
		||||
                    cv.Optional(CONF_END_FRAME): cv.positive_int,
 | 
			
		||||
                    cv.Optional(CONF_REPEAT): cv.positive_int,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
 | 
			
		||||
        },
 | 
			
		||||
        validate_cross_dependencies,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = ANIMATION_SCHEMA
 | 
			
		||||
 | 
			
		||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(Animation_),
 | 
			
		||||
@@ -80,26 +164,180 @@ async def animation_action_to_code(config, action_id, template_arg, args):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    (
 | 
			
		||||
        prog_arr,
 | 
			
		||||
        width,
 | 
			
		||||
        height,
 | 
			
		||||
        image_type,
 | 
			
		||||
        trans_value,
 | 
			
		||||
        frame_count,
 | 
			
		||||
    ) = await espImage.write_image(config, all_frames=True)
 | 
			
		||||
    from PIL import Image
 | 
			
		||||
 | 
			
		||||
    conf_file = config[CONF_FILE]
 | 
			
		||||
    if conf_file[CONF_SOURCE] == SOURCE_LOCAL:
 | 
			
		||||
        path = CORE.relative_config_path(conf_file[CONF_PATH])
 | 
			
		||||
    elif conf_file[CONF_SOURCE] == SOURCE_WEB:
 | 
			
		||||
        path = espImage.compute_local_image_path(conf_file).as_posix()
 | 
			
		||||
    else:
 | 
			
		||||
        raise core.EsphomeError(f"Unknown animation source: {conf_file[CONF_SOURCE]}")
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        image = Image.open(path)
 | 
			
		||||
    except Exception as e:
 | 
			
		||||
        raise core.EsphomeError(f"Could not load image file {path}: {e}")
 | 
			
		||||
 | 
			
		||||
    width, height = image.size
 | 
			
		||||
    frames = image.n_frames
 | 
			
		||||
    if CONF_RESIZE in config:
 | 
			
		||||
        new_width_max, new_height_max = config[CONF_RESIZE]
 | 
			
		||||
        ratio = min(new_width_max / width, new_height_max / height)
 | 
			
		||||
        width, height = int(width * ratio), int(height * ratio)
 | 
			
		||||
    elif width > 500 or height > 500:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            'The image "%s" you requested is very big. Please consider'
 | 
			
		||||
            " using the resize parameter.",
 | 
			
		||||
            path,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    transparent = config[CONF_USE_TRANSPARENCY]
 | 
			
		||||
 | 
			
		||||
    if config[CONF_TYPE] == "GRAYSCALE":
 | 
			
		||||
        data = [0 for _ in range(height * width * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("LA", dither=Image.Dither.NONE)
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
 | 
			
		||||
                )
 | 
			
		||||
            for pix, a in pixels:
 | 
			
		||||
                if transparent:
 | 
			
		||||
                    if pix == 1:
 | 
			
		||||
                        pix = 0
 | 
			
		||||
                    if a < 0x80:
 | 
			
		||||
                        pix = 1
 | 
			
		||||
 | 
			
		||||
                data[pos] = pix
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "RGBA":
 | 
			
		||||
        data = [0 for _ in range(height * width * 4 * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("RGBA")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
 | 
			
		||||
                )
 | 
			
		||||
            for pix in pixels:
 | 
			
		||||
                data[pos] = pix[0]
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = pix[1]
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = pix[2]
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = pix[3]
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] == "RGB24":
 | 
			
		||||
        data = [0 for _ in range(height * width * 3 * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("RGBA")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
 | 
			
		||||
                )
 | 
			
		||||
            for r, g, b, a in pixels:
 | 
			
		||||
                if transparent:
 | 
			
		||||
                    if r == 0 and g == 0 and b == 1:
 | 
			
		||||
                        b = 0
 | 
			
		||||
                    if a < 0x80:
 | 
			
		||||
                        r = 0
 | 
			
		||||
                        g = 0
 | 
			
		||||
                        b = 1
 | 
			
		||||
 | 
			
		||||
                data[pos] = r
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = g
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = b
 | 
			
		||||
                pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]:
 | 
			
		||||
        bytes_per_pixel = 3 if transparent else 2
 | 
			
		||||
        data = [0 for _ in range(height * width * bytes_per_pixel * frames)]
 | 
			
		||||
        pos = 0
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            frame = image.convert("RGBA")
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
            pixels = list(frame.getdata())
 | 
			
		||||
            if len(pixels) != height * width:
 | 
			
		||||
                raise core.EsphomeError(
 | 
			
		||||
                    f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height * width})"
 | 
			
		||||
                )
 | 
			
		||||
            for r, g, b, a in pixels:
 | 
			
		||||
                R = r >> 3
 | 
			
		||||
                G = g >> 2
 | 
			
		||||
                B = b >> 3
 | 
			
		||||
                rgb = (R << 11) | (G << 5) | B
 | 
			
		||||
                data[pos] = rgb >> 8
 | 
			
		||||
                pos += 1
 | 
			
		||||
                data[pos] = rgb & 0xFF
 | 
			
		||||
                pos += 1
 | 
			
		||||
                if transparent:
 | 
			
		||||
                    data[pos] = a
 | 
			
		||||
                    pos += 1
 | 
			
		||||
 | 
			
		||||
    elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]:
 | 
			
		||||
        width8 = ((width + 7) // 8) * 8
 | 
			
		||||
        data = [0 for _ in range((height * width8 // 8) * frames)]
 | 
			
		||||
        for frameIndex in range(frames):
 | 
			
		||||
            image.seek(frameIndex)
 | 
			
		||||
            if transparent:
 | 
			
		||||
                alpha = image.split()[-1]
 | 
			
		||||
                has_alpha = alpha.getextrema()[0] < 0xFF
 | 
			
		||||
            else:
 | 
			
		||||
                has_alpha = False
 | 
			
		||||
            frame = image.convert("1", dither=Image.Dither.NONE)
 | 
			
		||||
            if CONF_RESIZE in config:
 | 
			
		||||
                frame = frame.resize([width, height])
 | 
			
		||||
                if transparent:
 | 
			
		||||
                    alpha = alpha.resize([width, height])
 | 
			
		||||
            for x, y in [(i, j) for i in range(width) for j in range(height)]:
 | 
			
		||||
                if transparent and has_alpha:
 | 
			
		||||
                    if not alpha.getpixel((x, y)):
 | 
			
		||||
                        continue
 | 
			
		||||
                elif frame.getpixel((x, y)):
 | 
			
		||||
                    continue
 | 
			
		||||
 | 
			
		||||
                pos = x + y * width8 + (height * width8 * frameIndex)
 | 
			
		||||
                data[pos // 8] |= 0x80 >> (pos % 8)
 | 
			
		||||
    else:
 | 
			
		||||
        raise core.EsphomeError(
 | 
			
		||||
            f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    rhs = [HexInt(x) for x in data]
 | 
			
		||||
    prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 | 
			
		||||
    var = cg.new_Pvariable(
 | 
			
		||||
        config[CONF_ID],
 | 
			
		||||
        prog_arr,
 | 
			
		||||
        width,
 | 
			
		||||
        height,
 | 
			
		||||
        frame_count,
 | 
			
		||||
        image_type,
 | 
			
		||||
        trans_value,
 | 
			
		||||
        frames,
 | 
			
		||||
        espImage.IMAGE_TYPE[config[CONF_TYPE]],
 | 
			
		||||
    )
 | 
			
		||||
    cg.add(var.set_transparency(transparent))
 | 
			
		||||
    if loop_config := config.get(CONF_LOOP):
 | 
			
		||||
        start = loop_config[CONF_START_FRAME]
 | 
			
		||||
        end = loop_config.get(CONF_END_FRAME, frame_count)
 | 
			
		||||
        end = loop_config.get(CONF_END_FRAME, frames)
 | 
			
		||||
        count = loop_config.get(CONF_REPEAT, -1)
 | 
			
		||||
        cg.add(var.set_loop(start, end, count))
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ namespace esphome {
 | 
			
		||||
namespace animation {
 | 
			
		||||
 | 
			
		||||
Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count,
 | 
			
		||||
                     image::ImageType type, image::Transparency transparent)
 | 
			
		||||
    : Image(data_start, width, height, type, transparent),
 | 
			
		||||
                     image::ImageType type)
 | 
			
		||||
    : Image(data_start, width, height, type),
 | 
			
		||||
      animation_data_start_(data_start),
 | 
			
		||||
      current_frame_(0),
 | 
			
		||||
      animation_frame_count_(animation_frame_count),
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,7 @@ namespace animation {
 | 
			
		||||
 | 
			
		||||
class Animation : public image::Image {
 | 
			
		||||
 public:
 | 
			
		||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type,
 | 
			
		||||
            image::Transparency transparent);
 | 
			
		||||
  Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, image::ImageType type);
 | 
			
		||||
 | 
			
		||||
  uint32_t get_animation_frame_count() const;
 | 
			
		||||
  int get_current_frame() const;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import ble_client, climate
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_UNIT_OF_MEASUREMENT
 | 
			
		||||
from esphome.components import climate, ble_client
 | 
			
		||||
from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT
 | 
			
		||||
 | 
			
		||||
UNITS = {
 | 
			
		||||
    "f": "f",
 | 
			
		||||
@@ -17,9 +17,9 @@ Anova = anova_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    climate.climate_schema(Anova)
 | 
			
		||||
    .extend(
 | 
			
		||||
    climate.CLIMATE_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(Anova),
 | 
			
		||||
            cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
@@ -29,7 +29,8 @@ CONFIG_SCHEMA = (
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = await climate.new_climate(config)
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await climate.register_climate(var, config)
 | 
			
		||||
    await ble_client.register_ble_node(var, config)
 | 
			
		||||
    cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT]))
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ enum {  // APDS9306 registers
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
void APDS9306::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up APDS9306...");
 | 
			
		||||
 | 
			
		||||
  uint8_t id;
 | 
			
		||||
  if (!this->read_byte(APDS9306_PART_ID, &id)) {  // Part ID register
 | 
			
		||||
@@ -97,7 +97,7 @@ void APDS9306::dump_config() {
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    switch (this->error_code_) {
 | 
			
		||||
      case COMMUNICATION_FAILED:
 | 
			
		||||
        ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
        ESP_LOGE(TAG, "Communication with APDS9306 failed!");
 | 
			
		||||
        break;
 | 
			
		||||
      case WRONG_ID:
 | 
			
		||||
        ESP_LOGE(TAG, "APDS9306 has invalid id!");
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,8 @@
 | 
			
		||||
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ static const char *const TAG = "apds9960";
 | 
			
		||||
#define APDS9960_WRITE_BYTE(reg, value) APDS9960_ERROR_CHECK(this->write_byte(reg, value));
 | 
			
		||||
 | 
			
		||||
void APDS9960::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up APDS9960...");
 | 
			
		||||
  uint8_t id;
 | 
			
		||||
  if (!this->read_byte(0x92, &id)) {  // ID register
 | 
			
		||||
    this->error_code_ = COMMUNICATION_FAILED;
 | 
			
		||||
@@ -141,7 +141,7 @@ void APDS9960::dump_config() {
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    switch (this->error_code_) {
 | 
			
		||||
      case COMMUNICATION_FAILED:
 | 
			
		||||
        ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
        ESP_LOGE(TAG, "Communication with APDS9960 failed!");
 | 
			
		||||
        break;
 | 
			
		||||
      case WRONG_ID:
 | 
			
		||||
        ESP_LOGE(TAG, "APDS9960 has invalid id!");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING
 | 
			
		||||
 | 
			
		||||
from . import APDS9960, CONF_APDS9960_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["apds9960"]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,12 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from . import APDS9960, CONF_APDS9960_ID
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["apds9960"]
 | 
			
		||||
 
 | 
			
		||||
@@ -82,19 +82,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ENCRYPTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_KEY): validate_encryption_key,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _encryption_schema(config):
 | 
			
		||||
    if config is None:
 | 
			
		||||
        config = {}
 | 
			
		||||
    return ENCRYPTION_SCHEMA(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -108,7 +95,11 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
 | 
			
		||||
            ): ACTIONS_SCHEMA,
 | 
			
		||||
            cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
 | 
			
		||||
            cv.Optional(CONF_ENCRYPTION): _encryption_schema,
 | 
			
		||||
            cv.Optional(CONF_ENCRYPTION): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_KEY): validate_encryption_key,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
 | 
			
		||||
                single=True
 | 
			
		||||
            ),
 | 
			
		||||
@@ -160,17 +151,9 @@ async def to_code(config):
 | 
			
		||||
            config[CONF_ON_CLIENT_DISCONNECTED],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
 | 
			
		||||
        if key := encryption_config.get(CONF_KEY):
 | 
			
		||||
            decoded = base64.b64decode(key)
 | 
			
		||||
            cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
        else:
 | 
			
		||||
            # No key provided, but encryption desired
 | 
			
		||||
            # This will allow a plaintext client to provide a noise key,
 | 
			
		||||
            # send it to the device, and then switch to noise.
 | 
			
		||||
            # The key will be saved in flash and used for future connections
 | 
			
		||||
            # and plaintext disabled. Only a factory reset can remove it.
 | 
			
		||||
            cg.add_define("USE_API_PLAINTEXT")
 | 
			
		||||
    if encryption_config := config.get(CONF_ENCRYPTION):
 | 
			
		||||
        decoded = base64.b64decode(encryption_config[CONF_KEY])
 | 
			
		||||
        cg.add(var.set_noise_psk(list(decoded)))
 | 
			
		||||
        cg.add_define("USE_API_NOISE")
 | 
			
		||||
        cg.add_library("esphome/noise-c", "0.1.6")
 | 
			
		||||
    else:
 | 
			
		||||
 
 | 
			
		||||
@@ -31,26 +31,24 @@ service APIConnection {
 | 
			
		||||
    option (needs_authentication) = false;
 | 
			
		||||
  }
 | 
			
		||||
  rpc execute_service (ExecuteServiceRequest) returns (void) {}
 | 
			
		||||
  rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {}
 | 
			
		||||
 | 
			
		||||
  rpc button_command (ButtonCommandRequest) returns (void) {}
 | 
			
		||||
  rpc camera_image (CameraImageRequest) returns (void) {}
 | 
			
		||||
  rpc climate_command (ClimateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc cover_command (CoverCommandRequest) returns (void) {}
 | 
			
		||||
  rpc date_command (DateCommandRequest) 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 camera_image (CameraImageRequest) returns (void) {}
 | 
			
		||||
  rpc climate_command (ClimateCommandRequest) returns (void) {}
 | 
			
		||||
  rpc number_command (NumberCommandRequest) returns (void) {}
 | 
			
		||||
  rpc text_command (TextCommandRequest) returns (void) {}
 | 
			
		||||
  rpc time_command (TimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc update_command (UpdateCommandRequest) 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 time_command (TimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc datetime_command (DateTimeCommandRequest) returns (void) {}
 | 
			
		||||
  rpc update_command (UpdateCommandRequest) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
 | 
			
		||||
  rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
 | 
			
		||||
@@ -62,7 +60,6 @@ service APIConnection {
 | 
			
		||||
  rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {}
 | 
			
		||||
  rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
 | 
			
		||||
  rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
 | 
			
		||||
  rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
 | 
			
		||||
 | 
			
		||||
  rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
 | 
			
		||||
  rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
 | 
			
		||||
@@ -230,12 +227,6 @@ message DeviceInfoResponse {
 | 
			
		||||
  uint32 voice_assistant_feature_flags = 17;
 | 
			
		||||
 | 
			
		||||
  string suggested_area = 16;
 | 
			
		||||
 | 
			
		||||
  // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
 | 
			
		||||
  string bluetooth_mac_address = 18;
 | 
			
		||||
 | 
			
		||||
  // Supports receiving and saving api encryption key
 | 
			
		||||
  bool api_encryption_supported = 19;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message ListEntitiesRequest {
 | 
			
		||||
@@ -432,8 +423,7 @@ message FanCommandRequest {
 | 
			
		||||
enum ColorMode {
 | 
			
		||||
  COLOR_MODE_UNKNOWN = 0;
 | 
			
		||||
  COLOR_MODE_ON_OFF = 1;
 | 
			
		||||
  COLOR_MODE_LEGACY_BRIGHTNESS = 2;
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 3;
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 2;
 | 
			
		||||
  COLOR_MODE_WHITE = 7;
 | 
			
		||||
  COLOR_MODE_COLOR_TEMPERATURE = 11;
 | 
			
		||||
  COLOR_MODE_COLD_WARM_WHITE = 19;
 | 
			
		||||
@@ -657,27 +647,10 @@ message SubscribeLogsResponse {
 | 
			
		||||
  option (no_delay) = false;
 | 
			
		||||
 | 
			
		||||
  LogLevel level = 1;
 | 
			
		||||
  bytes message = 3;
 | 
			
		||||
  string message = 3;
 | 
			
		||||
  bool send_failed = 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== NOISE ENCRYPTION ====================
 | 
			
		||||
message NoiseEncryptionSetKeyRequest {
 | 
			
		||||
  option (id) = 124;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (ifdef) = "USE_API_NOISE";
 | 
			
		||||
 | 
			
		||||
  bytes key = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message NoiseEncryptionSetKeyResponse {
 | 
			
		||||
  option (id) = 125;
 | 
			
		||||
  option (source) = SOURCE_SERVER;
 | 
			
		||||
  option (ifdef) = "USE_API_NOISE";
 | 
			
		||||
 | 
			
		||||
  bool success = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== HOMEASSISTANT.SERVICE ====================
 | 
			
		||||
message SubscribeHomeassistantServicesRequest {
 | 
			
		||||
  option (id) = 34;
 | 
			
		||||
@@ -913,7 +886,6 @@ message ClimateStateResponse {
 | 
			
		||||
  float target_temperature = 4;
 | 
			
		||||
  float target_temperature_low = 5;
 | 
			
		||||
  float target_temperature_high = 6;
 | 
			
		||||
  // For older peers, equal to preset == CLIMATE_PRESET_AWAY
 | 
			
		||||
  bool unused_legacy_away = 7;
 | 
			
		||||
  ClimateAction action = 8;
 | 
			
		||||
  ClimateFanMode fan_mode = 9;
 | 
			
		||||
@@ -939,7 +911,6 @@ message ClimateCommandRequest {
 | 
			
		||||
  float target_temperature_low = 7;
 | 
			
		||||
  bool has_target_temperature_high = 8;
 | 
			
		||||
  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_legacy_away = 11;
 | 
			
		||||
  bool has_fan_mode = 12;
 | 
			
		||||
@@ -1042,49 +1013,6 @@ message SelectCommandRequest {
 | 
			
		||||
  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 ====================
 | 
			
		||||
enum LockState {
 | 
			
		||||
@@ -1254,8 +1182,8 @@ message SubscribeBluetoothLEAdvertisementsRequest {
 | 
			
		||||
 | 
			
		||||
message BluetoothServiceData {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
  repeated uint32 legacy_data = 2 [deprecated = true];  // Removed in api version 1.7
 | 
			
		||||
  bytes data = 3;  // Added in api version 1.7
 | 
			
		||||
  repeated uint32 legacy_data = 2 [deprecated = true];
 | 
			
		||||
  bytes data = 3; // Changed in proto version 1.7
 | 
			
		||||
}
 | 
			
		||||
message BluetoothLEAdvertisementResponse {
 | 
			
		||||
  option (id) = 67;
 | 
			
		||||
@@ -1264,7 +1192,7 @@ message BluetoothLEAdvertisementResponse {
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  uint64 address = 1;
 | 
			
		||||
  bytes name = 2;
 | 
			
		||||
  string name = 2;
 | 
			
		||||
  sint32 rssi = 3;
 | 
			
		||||
 | 
			
		||||
  repeated string service_uuids = 4;
 | 
			
		||||
@@ -1453,7 +1381,6 @@ message BluetoothConnectionsFreeResponse {
 | 
			
		||||
 | 
			
		||||
  uint32 free = 1;
 | 
			
		||||
  uint32 limit = 2;
 | 
			
		||||
  repeated uint64 allocated = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothGATTErrorResponse {
 | 
			
		||||
@@ -1520,38 +1447,7 @@ message BluetoothDeviceClearCacheResponse {
 | 
			
		||||
  int32 error = 3;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BluetoothScannerState {
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_IDLE = 0;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STARTING = 1;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_RUNNING = 2;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_FAILED = 3;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STOPPING = 4;
 | 
			
		||||
  BLUETOOTH_SCANNER_STATE_STOPPED = 5;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BluetoothScannerMode {
 | 
			
		||||
  BLUETOOTH_SCANNER_MODE_PASSIVE = 0;
 | 
			
		||||
  BLUETOOTH_SCANNER_MODE_ACTIVE = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothScannerStateResponse {
 | 
			
		||||
  option(id) = 126;
 | 
			
		||||
  option(source) = SOURCE_SERVER;
 | 
			
		||||
  option(ifdef) = "USE_BLUETOOTH_PROXY";
 | 
			
		||||
 | 
			
		||||
  BluetoothScannerState state = 1;
 | 
			
		||||
  BluetoothScannerMode mode = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message BluetoothScannerSetModeRequest {
 | 
			
		||||
  option(id) = 127;
 | 
			
		||||
  option(source) = SOURCE_CLIENT;
 | 
			
		||||
  option(ifdef) = "USE_BLUETOOTH_PROXY";
 | 
			
		||||
 | 
			
		||||
  BluetoothScannerMode mode = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== VOICE ASSISTANT ====================
 | 
			
		||||
// ==================== PUSH TO TALK ====================
 | 
			
		||||
enum VoiceAssistantSubscribeFlag {
 | 
			
		||||
  VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
 | 
			
		||||
  VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
 | 
			
		||||
@@ -1667,8 +1563,6 @@ message VoiceAssistantAnnounceRequest {
 | 
			
		||||
 | 
			
		||||
  string media_id = 1;
 | 
			
		||||
  string text = 2;
 | 
			
		||||
  string preannounce_media_id = 3;
 | 
			
		||||
  bool start_conversation = 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message VoiceAssistantAnnounceFinished {
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,57 +8,12 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
// 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
 | 
			
		||||
  will lazily publish that message.  The two pointers allow dedup in the deferred queue if multiple publishes for the
 | 
			
		||||
  same component are backed up, and take up only 8 bytes of memory.  The entry in the deferred queue (a std::vector) is
 | 
			
		||||
  the DeferredMessage instance itself (not a pointer to one elsewhere in heap) so still only 8 bytes per entry.  Even
 | 
			
		||||
  100 backed up messages (you'd have to have at least 100 sensors publishing because of dedup) would take up only 0.8
 | 
			
		||||
  kB.
 | 
			
		||||
*/
 | 
			
		||||
class DeferredMessageQueue {
 | 
			
		||||
  struct DeferredMessage {
 | 
			
		||||
    friend class DeferredMessageQueue;
 | 
			
		||||
 | 
			
		||||
   protected:
 | 
			
		||||
    void *source_;
 | 
			
		||||
    send_message_t send_message_;
 | 
			
		||||
 | 
			
		||||
   public:
 | 
			
		||||
    DeferredMessage(void *source, send_message_t send_message) : source_(source), send_message_(send_message) {}
 | 
			
		||||
    bool operator==(const DeferredMessage &test) const {
 | 
			
		||||
      return (source_ == test.source_ && send_message_ == test.send_message_);
 | 
			
		||||
    }
 | 
			
		||||
  } __attribute__((packed));
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // vector is used very specifically for its zero memory overhead even though items are popped from the front (memory
 | 
			
		||||
  // footprint is more important than speed here)
 | 
			
		||||
  std::vector<DeferredMessage> deferred_queue_;
 | 
			
		||||
  APIConnection *api_connection_;
 | 
			
		||||
 | 
			
		||||
  // helper for allowing only unique entries in the queue
 | 
			
		||||
  void dmq_push_back_with_dedup_(void *source, send_message_t send_message);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  DeferredMessageQueue(APIConnection *api_connection) : api_connection_(api_connection) {}
 | 
			
		||||
  void process_queue();
 | 
			
		||||
  void defer(void *source, send_message_t send_message);
 | 
			
		||||
  bool empty() const { return deferred_queue_.empty(); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class APIConnection : public APIServerConnection {
 | 
			
		||||
 public:
 | 
			
		||||
  APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
 | 
			
		||||
@@ -73,216 +28,96 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  bool send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor, bool state);
 | 
			
		||||
  void send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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:
 | 
			
		||||
  bool send_binary_sensor_info(binary_sensor::BinarySensor *binary_sensor);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_COVER
 | 
			
		||||
  bool send_cover_state(cover::Cover *cover);
 | 
			
		||||
  void send_cover_info(cover::Cover *cover);
 | 
			
		||||
  bool send_cover_info(cover::Cover *cover);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_FAN
 | 
			
		||||
  bool send_fan_state(fan::Fan *fan);
 | 
			
		||||
  void send_fan_info(fan::Fan *fan);
 | 
			
		||||
  bool send_fan_info(fan::Fan *fan);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_LIGHT
 | 
			
		||||
  bool send_light_state(light::LightState *light);
 | 
			
		||||
  void send_light_info(light::LightState *light);
 | 
			
		||||
  bool send_light_info(light::LightState *light);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  bool send_sensor_state(sensor::Sensor *sensor, float state);
 | 
			
		||||
  void send_sensor_info(sensor::Sensor *sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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:
 | 
			
		||||
  bool send_sensor_info(sensor::Sensor *sensor);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  bool send_switch_state(switch_::Switch *a_switch, bool state);
 | 
			
		||||
  void send_switch_info(switch_::Switch *a_switch);
 | 
			
		||||
  bool send_switch_info(switch_::Switch *a_switch);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  bool send_text_sensor_state(text_sensor::TextSensor *text_sensor, std::string state);
 | 
			
		||||
  void send_text_sensor_info(text_sensor::TextSensor *text_sensor);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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:
 | 
			
		||||
  bool send_text_sensor_info(text_sensor::TextSensor *text_sensor);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  void set_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
 | 
			
		||||
  void send_camera_info(esp32_camera::ESP32Camera *camera);
 | 
			
		||||
  void send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image);
 | 
			
		||||
  bool send_camera_info(esp32_camera::ESP32Camera *camera);
 | 
			
		||||
  void camera_image(const CameraImageRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_camera_info_(esp32_camera::ESP32Camera *camera);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_CLIMATE
 | 
			
		||||
  bool send_climate_state(climate::Climate *climate);
 | 
			
		||||
  void send_climate_info(climate::Climate *climate);
 | 
			
		||||
  bool send_climate_info(climate::Climate *climate);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  bool send_number_state(number::Number *number, float state);
 | 
			
		||||
  void send_number_info(number::Number *number);
 | 
			
		||||
  bool send_number_info(number::Number *number);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_DATETIME_DATE
 | 
			
		||||
  bool send_date_state(datetime::DateEntity *date);
 | 
			
		||||
  void send_date_info(datetime::DateEntity *date);
 | 
			
		||||
  bool send_date_info(datetime::DateEntity *date);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_DATETIME_TIME
 | 
			
		||||
  bool send_time_state(datetime::TimeEntity *time);
 | 
			
		||||
  void send_time_info(datetime::TimeEntity *time);
 | 
			
		||||
  bool send_time_info(datetime::TimeEntity *time);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_DATETIME_DATETIME
 | 
			
		||||
  bool send_datetime_state(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  void send_datetime_info(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  bool send_datetime_info(datetime::DateTimeEntity *datetime);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_TEXT
 | 
			
		||||
  bool send_text_state(text::Text *text, std::string state);
 | 
			
		||||
  void send_text_info(text::Text *text);
 | 
			
		||||
  bool send_text_info(text::Text *text);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  bool send_select_state(select::Select *select, std::string state);
 | 
			
		||||
  void send_select_info(select::Select *select);
 | 
			
		||||
  bool send_select_info(select::Select *select);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  void send_button_info(button::Button *button);
 | 
			
		||||
  bool send_button_info(button::Button *button);
 | 
			
		||||
  void button_command(const ButtonCommandRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool try_send_button_info_(button::Button *button);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_LOCK
 | 
			
		||||
  bool send_lock_state(lock::Lock *a_lock, lock::LockState state);
 | 
			
		||||
  void send_lock_info(lock::Lock *a_lock);
 | 
			
		||||
  bool send_lock_info(lock::Lock *a_lock);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_VALVE
 | 
			
		||||
  bool send_valve_state(valve::Valve *valve);
 | 
			
		||||
  void send_valve_info(valve::Valve *valve);
 | 
			
		||||
  bool send_valve_info(valve::Valve *valve);
 | 
			
		||||
  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
 | 
			
		||||
#ifdef USE_MEDIA_PLAYER
 | 
			
		||||
  bool send_media_player_state(media_player::MediaPlayer *media_player);
 | 
			
		||||
  void send_media_player_info(media_player::MediaPlayer *media_player);
 | 
			
		||||
  bool send_media_player_info(media_player::MediaPlayer *media_player);
 | 
			
		||||
  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
 | 
			
		||||
  bool try_send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  bool send_log_message(int level, const char *tag, const char *line);
 | 
			
		||||
  void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
 | 
			
		||||
    if (!this->service_call_subscription_)
 | 
			
		||||
      return;
 | 
			
		||||
@@ -302,7 +137,6 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
 | 
			
		||||
  BluetoothConnectionsFreeResponse subscribe_bluetooth_connections_free(
 | 
			
		||||
      const SubscribeBluetoothConnectionsFreeRequest &msg) override;
 | 
			
		||||
  void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
@@ -326,38 +160,19 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_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);
 | 
			
		||||
  bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
#ifdef USE_EVENT
 | 
			
		||||
  void send_event(event::Event *event, std::string event_type);
 | 
			
		||||
  void send_event_info(event::Event *event);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  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:
 | 
			
		||||
  bool send_event(event::Event *event, std::string event_type);
 | 
			
		||||
  bool send_event_info(event::Event *event);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
  bool send_update_state(update::UpdateEntity *update);
 | 
			
		||||
  void send_update_info(update::UpdateEntity *update);
 | 
			
		||||
  bool send_update_info(update::UpdateEntity *update);
 | 
			
		||||
  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
 | 
			
		||||
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override;
 | 
			
		||||
@@ -394,9 +209,6 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  void execute_service(const ExecuteServiceRequest &msg) override;
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  NoiseEncryptionSetKeyResponse noise_encryption_set_key(const NoiseEncryptionSetKeyRequest &msg) override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; }
 | 
			
		||||
  bool is_connection_setup() override {
 | 
			
		||||
@@ -405,20 +217,11 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void on_fatal_error() override;
 | 
			
		||||
  void on_unauthenticated_access() override;
 | 
			
		||||
  void on_no_setup_connection() override;
 | 
			
		||||
  ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
 | 
			
		||||
  ProtoWriteBuffer create_buffer() override {
 | 
			
		||||
    // FIXME: ensure no recursive writes can happen
 | 
			
		||||
    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_};
 | 
			
		||||
  }
 | 
			
		||||
  bool try_to_clear_buffer(bool log_out_of_space);
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override;
 | 
			
		||||
 | 
			
		||||
  std::string get_client_combined_info() const { return this->client_combined_info_; }
 | 
			
		||||
@@ -426,99 +229,6 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
 protected:
 | 
			
		||||
  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);
 | 
			
		||||
 | 
			
		||||
  enum class ConnectionState {
 | 
			
		||||
@@ -552,7 +262,6 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool service_call_subscription_{false};
 | 
			
		||||
  bool next_close_ = false;
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
  DeferredMessageQueue deferred_message_queue_;
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
  int state_subs_at_ = -1;
 | 
			
		||||
 
 | 
			
		||||
@@ -5,15 +5,21 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "proto.h"
 | 
			
		||||
#include "api_pb2_size.h"
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.socket";
 | 
			
		||||
 | 
			
		||||
/// Is the given return value (from write syscalls) a wouldblock error?
 | 
			
		||||
bool is_would_block(ssize_t ret) {
 | 
			
		||||
  if (ret == -1) {
 | 
			
		||||
    return errno == EWOULDBLOCK || errno == EAGAIN;
 | 
			
		||||
  }
 | 
			
		||||
  return ret == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *api_error_to_str(APIError err) {
 | 
			
		||||
  // not using switch to ensure compiler doesn't try to build a big table out of it
 | 
			
		||||
  if (err == APIError::OK) {
 | 
			
		||||
@@ -66,154 +72,7 @@ const char *api_error_to_str(APIError err) {
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper method to buffer data from IOVs
 | 
			
		||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
 | 
			
		||||
  SendBuffer buffer;
 | 
			
		||||
  buffer.data.reserve(total_write_len);
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
    const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
 | 
			
		||||
    buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
 | 
			
		||||
  }
 | 
			
		||||
  this->tx_buf_.push_back(std::move(buffer));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This method writes data to socket or buffers it
 | 
			
		||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
  // Returns APIError::OK if successful (or would block, but data has been buffered)
 | 
			
		||||
  // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
 | 
			
		||||
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;  // Nothing to do, success
 | 
			
		||||
 | 
			
		||||
  uint16_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 += static_cast<uint16_t>(iov[i].iov_len);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Try to send any existing buffered data first if there is any
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
    APIError send_result = try_send_tx_buf_();
 | 
			
		||||
    // If real error occurred (not just WOULD_BLOCK), return it
 | 
			
		||||
    if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
 | 
			
		||||
      return send_result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If there is still data in the buffer, we can't send, buffer
 | 
			
		||||
    // the new data and return
 | 
			
		||||
    if (!this->tx_buf_.empty()) {
 | 
			
		||||
      this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
 | 
			
		||||
      return APIError::OK;  // Success, data buffered
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Try to send directly if no buffered data
 | 
			
		||||
  ssize_t sent = this->socket_->writev(iov, iovcnt);
 | 
			
		||||
 | 
			
		||||
  if (sent == -1) {
 | 
			
		||||
    if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
      // Socket would block, buffer the data
 | 
			
		||||
      this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
 | 
			
		||||
      return APIError::OK;  // Success, data buffered
 | 
			
		||||
    }
 | 
			
		||||
    // Socket error
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
  } else if (static_cast<uint16_t>(sent) < total_write_len) {
 | 
			
		||||
    // Partially sent, buffer the remaining data
 | 
			
		||||
    SendBuffer buffer;
 | 
			
		||||
    uint16_t to_consume = static_cast<uint16_t>(sent);
 | 
			
		||||
    uint16_t remaining = total_write_len - static_cast<uint16_t>(sent);
 | 
			
		||||
 | 
			
		||||
    buffer.data.reserve(remaining);
 | 
			
		||||
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      if (to_consume >= iov[i].iov_len) {
 | 
			
		||||
        // This segment was fully sent
 | 
			
		||||
        to_consume -= static_cast<uint16_t>(iov[i].iov_len);
 | 
			
		||||
      } else {
 | 
			
		||||
        // This segment was partially sent or not sent at all
 | 
			
		||||
        const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
 | 
			
		||||
        uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_consume;
 | 
			
		||||
        buffer.data.insert(buffer.data.end(), data, data + len);
 | 
			
		||||
        to_consume = 0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->tx_buf_.push_back(std::move(buffer));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;  // Success, all data sent or buffered
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for trying to send buffered data
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
 | 
			
		||||
APIError APIFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
 | 
			
		||||
  bool tx_buf_empty = false;
 | 
			
		||||
  while (!tx_buf_empty) {
 | 
			
		||||
    // Get the first buffer in the queue
 | 
			
		||||
    SendBuffer &front_buffer = this->tx_buf_.front();
 | 
			
		||||
 | 
			
		||||
    // Try to send the remaining data in this buffer
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
 | 
			
		||||
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno != EWOULDBLOCK && errno != EAGAIN) {
 | 
			
		||||
        // Real socket error (not just would block)
 | 
			
		||||
        ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
        this->state_ = State::FAILED;
 | 
			
		||||
        return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
      }
 | 
			
		||||
      // Socket would block, we'll try again later
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      // Nothing sent but not an error
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
 | 
			
		||||
      // Partially sent, update offset
 | 
			
		||||
      // Cast to ensure no overflow issues with uint16_t
 | 
			
		||||
      front_buffer.offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      return APIError::WOULD_BLOCK;  // Stop processing more buffers if we couldn't send a complete buffer
 | 
			
		||||
    } else {
 | 
			
		||||
      // Buffer completely sent, remove it from the queue
 | 
			
		||||
      this->tx_buf_.pop_front();
 | 
			
		||||
      // Update empty status for the loop condition
 | 
			
		||||
      tx_buf_empty = this->tx_buf_.empty();
 | 
			
		||||
      // Continue loop to try sending the next buffer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;  // All buffers sent successfully
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIFrameHelper::init_common_() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = this->socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
@@ -261,9 +120,23 @@ std::string noise_err_to_str(int err) {
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APINoiseFrameHelper::init() {
 | 
			
		||||
  APIError err = init_common_();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // init prologue
 | 
			
		||||
@@ -275,16 +148,17 @@ APIError APINoiseFrameHelper::init() {
 | 
			
		||||
/// Run through handshake messages (if in that phase)
 | 
			
		||||
APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  APIError err = state_action_();
 | 
			
		||||
  if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (err != APIError::OK)
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
@@ -310,8 +184,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
    uint8_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    size_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -324,8 +198,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_len_ += static_cast<uint8_t>(received);
 | 
			
		||||
    if (static_cast<uint8_t>(received) != to_read) {
 | 
			
		||||
    rx_header_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
      // not a full read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -357,8 +231,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    uint16_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    size_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -371,8 +245,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -421,8 +295,6 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
      return aerr;
 | 
			
		||||
    // ignore contents, may be used in future for flags
 | 
			
		||||
    // Reserve space for: existing prologue + 2 size bytes + frame data
 | 
			
		||||
    prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
 | 
			
		||||
    prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
 | 
			
		||||
    prologue_.push_back((uint8_t) frame.msg.size());
 | 
			
		||||
    prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
 | 
			
		||||
@@ -431,22 +303,14 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::SERVER_HELLO) {
 | 
			
		||||
    // send server hello
 | 
			
		||||
    const std::string &name = App.get_name();
 | 
			
		||||
    const std::string &mac = get_mac_address();
 | 
			
		||||
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
    // Reserve space for: 1 byte proto + name + null + mac + null
 | 
			
		||||
    msg.reserve(1 + name.size() + 1 + mac.size() + 1);
 | 
			
		||||
 | 
			
		||||
    // chosen proto
 | 
			
		||||
    msg.push_back(0x01);
 | 
			
		||||
 | 
			
		||||
    // node name, terminated by null byte
 | 
			
		||||
    const std::string &name = App.get_name();
 | 
			
		||||
    const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
 | 
			
		||||
    msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
 | 
			
		||||
    // node mac, terminated by null byte
 | 
			
		||||
    const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
 | 
			
		||||
    msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
 | 
			
		||||
 | 
			
		||||
    aerr = write_frame_(msg.data(), msg.size());
 | 
			
		||||
    if (aerr != APIError::OK)
 | 
			
		||||
@@ -539,18 +403,16 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
 | 
			
		||||
  std::vector<uint8_t> data;
 | 
			
		||||
  data.resize(reason.length() + 1);
 | 
			
		||||
  data[0] = 0x01;  // failure
 | 
			
		||||
 | 
			
		||||
  // Copy error message in bulk
 | 
			
		||||
  if (!reason.empty()) {
 | 
			
		||||
    std::memcpy(data.data() + 1, reason.c_str(), reason.length());
 | 
			
		||||
  for (size_t i = 0; i < reason.length(); i++) {
 | 
			
		||||
    data[i + 1] = (uint8_t) reason[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // temporarily remove failed state
 | 
			
		||||
  auto orig_state = state_;
 | 
			
		||||
  state_ = State::EXPLICIT_REJECT;
 | 
			
		||||
  write_frame_(data.data(), data.size());
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
@@ -578,7 +440,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
    return APIError::CIPHERSTATE_DECRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t msg_size = mbuf.size;
 | 
			
		||||
  size_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.msg.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
@@ -604,7 +466,8 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
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) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
@@ -616,36 +479,31 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding
 | 
			
		||||
  uint16_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
  uint16_t padding = 0;
 | 
			
		||||
  uint16_t msg_len = 4 + payload_len + padding;
 | 
			
		||||
  size_t padding = 0;
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We need to resize to include MAC space, but we already reserved it in create_buffer
 | 
			
		||||
  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
 | 
			
		||||
  tmpbuf[0] = 0x01;  // indicator
 | 
			
		||||
  // tmpbuf[1], tmpbuf[2] to be set later
 | 
			
		||||
  const uint8_t msg_offset = 3;
 | 
			
		||||
  buf_start[msg_offset + 0] = (uint8_t) (type >> 8);         // type high byte
 | 
			
		||||
  buf_start[msg_offset + 1] = (uint8_t) type;                // type low byte
 | 
			
		||||
  buf_start[msg_offset + 2] = (uint8_t) (payload_len >> 8);  // data_len high byte
 | 
			
		||||
  buf_start[msg_offset + 3] = (uint8_t) payload_len;         // data_len low byte
 | 
			
		||||
  // payload data is already in the buffer starting at position 7
 | 
			
		||||
  const uint8_t payload_offset = msg_offset + 4;
 | 
			
		||||
  tmpbuf[msg_offset + 0] = (uint8_t) (type >> 8);  // type
 | 
			
		||||
  tmpbuf[msg_offset + 1] = (uint8_t) type;
 | 
			
		||||
  tmpbuf[msg_offset + 2] = (uint8_t) (payload_len >> 8);  // data_len
 | 
			
		||||
  tmpbuf[msg_offset + 3] = (uint8_t) payload_len;
 | 
			
		||||
  // 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;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  // 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_);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset);
 | 
			
		||||
  err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
@@ -653,20 +511,103 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
 | 
			
		||||
    return APIError::CIPHERSTATE_ENCRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t total_len = 3 + mbuf.size;
 | 
			
		||||
  buf_start[1] = (uint8_t) (mbuf.size >> 8);
 | 
			
		||||
  buf_start[2] = (uint8_t) mbuf.size;
 | 
			
		||||
  size_t total_len = 3 + mbuf.size;
 | 
			
		||||
  tmpbuf[1] = (uint8_t) (mbuf.size >> 8);
 | 
			
		||||
  tmpbuf[2] = (uint8_t) mbuf.size;
 | 
			
		||||
 | 
			
		||||
  struct iovec iov;
 | 
			
		||||
  // 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_base = &tmpbuf[0];
 | 
			
		||||
  iov.iov_len = total_len;
 | 
			
		||||
 | 
			
		||||
  // write raw to not have two packets sent if NAGLE disabled
 | 
			
		||||
  return this->write_raw_(&iov, 1);
 | 
			
		||||
  return write_raw_(&iov, 1);
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
 | 
			
		||||
APIError APINoiseFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN)
 | 
			
		||||
        break;
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
/** 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) {
 | 
			
		||||
  uint8_t header[3];
 | 
			
		||||
  header[0] = 0x01;  // indicator
 | 
			
		||||
  header[1] = (uint8_t) (len >> 8);
 | 
			
		||||
@@ -676,12 +617,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
 | 
			
		||||
  iov[0].iov_base = header;
 | 
			
		||||
  iov[0].iov_len = 3;
 | 
			
		||||
  if (len == 0) {
 | 
			
		||||
    return this->write_raw_(iov, 1);
 | 
			
		||||
    return write_raw_(iov, 1);
 | 
			
		||||
  }
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(data);
 | 
			
		||||
  iov[1].iov_len = len;
 | 
			
		||||
 | 
			
		||||
  return this->write_raw_(iov, 2);
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Initiate the data structures for the handshake.
 | 
			
		||||
@@ -752,8 +693,6 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
 | 
			
		||||
    return APIError::HANDSHAKESTATE_SPLIT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
 | 
			
		||||
 | 
			
		||||
  HELPER_LOG("Handshake complete!");
 | 
			
		||||
  noise_handshakestate_free(handshake_);
 | 
			
		||||
  handshake_ = nullptr;
 | 
			
		||||
@@ -776,6 +715,22 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
extern "C" {
 | 
			
		||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
 | 
			
		||||
void noise_rand_bytes(void *output, size_t len) {
 | 
			
		||||
@@ -785,16 +740,28 @@ void noise_rand_bytes(void *output, size_t len) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APIPlaintextFrameHelper::init() {
 | 
			
		||||
  APIError err = init_common_();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  if (state_ != State::INITIALIZE || socket_ == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad state for init %d", (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nonblocking failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Setting nodelay failed with errno %d", errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
@@ -805,13 +772,14 @@ APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  // try send pending TX data
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
@@ -831,15 +799,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    // Now that we know when the socket is ready, we can read up to 3 bytes
 | 
			
		||||
    // into the rx_header_buf_ before we have to switch back to reading
 | 
			
		||||
    // one byte at a time to ensure we don't read past the message and
 | 
			
		||||
    // into the next one.
 | 
			
		||||
 | 
			
		||||
    // Read directly into rx_header_buf_ at the current position
 | 
			
		||||
    // Try to get to at least 3 bytes total (indicator + 2 varint bytes), then read one byte at a time
 | 
			
		||||
    ssize_t received =
 | 
			
		||||
        this->socket_->read(&rx_header_buf_[rx_header_buf_pos_], rx_header_buf_pos_ < 3 ? 3 - rx_header_buf_pos_ : 1);
 | 
			
		||||
    uint8_t data;
 | 
			
		||||
    ssize_t received = socket_->read(&data, 1);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -852,75 +813,32 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_.push_back(data);
 | 
			
		||||
 | 
			
		||||
    // If this was the first read, validate the indicator byte
 | 
			
		||||
    if (rx_header_buf_pos_ == 0 && received > 0) {
 | 
			
		||||
      if (rx_header_buf_[0] != 0x00) {
 | 
			
		||||
        state_ = State::FAILED;
 | 
			
		||||
        HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
 | 
			
		||||
        return APIError::BAD_INDICATOR;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    rx_header_buf_pos_ += received;
 | 
			
		||||
 | 
			
		||||
    // Check for buffer overflow
 | 
			
		||||
    if (rx_header_buf_pos_ >= sizeof(rx_header_buf_)) {
 | 
			
		||||
    // try parse header
 | 
			
		||||
    if (rx_header_buf_[0] != 0x00) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Header buffer overflow");
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
      HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
 | 
			
		||||
      return APIError::BAD_INDICATOR;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Need at least 3 bytes total (indicator + 2 varint bytes) before trying to parse
 | 
			
		||||
    if (rx_header_buf_pos_ < 3) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // At this point, we have at least 3 bytes total:
 | 
			
		||||
    //   - Validated indicator byte (0x00) stored at position 0
 | 
			
		||||
    //   - At least 2 bytes in the buffer for the varints
 | 
			
		||||
    // Buffer layout:
 | 
			
		||||
    //   [0]: indicator byte (0x00)
 | 
			
		||||
    //   [1-3]: Message size varint (variable length)
 | 
			
		||||
    //     - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
 | 
			
		||||
    //     - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
 | 
			
		||||
    //   [2-5]: Message type varint (variable length)
 | 
			
		||||
    // We now attempt to parse both varints. If either is incomplete,
 | 
			
		||||
    // we'll continue reading more bytes.
 | 
			
		||||
 | 
			
		||||
    // Skip indicator byte at position 0
 | 
			
		||||
    uint8_t varint_pos = 1;
 | 
			
		||||
    size_t i = 1;
 | 
			
		||||
    uint32_t consumed = 0;
 | 
			
		||||
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
 | 
			
		||||
    auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    if (!msg_size_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
 | 
			
		||||
                 std::numeric_limits<uint16_t>::max());
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint16();
 | 
			
		||||
    i += consumed;
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint32();
 | 
			
		||||
 | 
			
		||||
    // Move to next varint position
 | 
			
		||||
    varint_pos += consumed;
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
 | 
			
		||||
                 std::numeric_limits<uint16_t>::max());
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint16();
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint32();
 | 
			
		||||
    rx_header_parsed_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
@@ -932,8 +850,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -946,8 +864,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -961,10 +879,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_pos_ = 0;
 | 
			
		||||
  rx_header_buf_.clear();
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
@@ -974,28 +893,8 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      // Make sure to tell the remote that we don't
 | 
			
		||||
      // understand the indicator byte so it knows
 | 
			
		||||
      // we do not support it.
 | 
			
		||||
      struct iovec iov[1];
 | 
			
		||||
      // The \x00 first byte is the marker for plaintext.
 | 
			
		||||
      //
 | 
			
		||||
      // The remote will know how to handle the indicator byte,
 | 
			
		||||
      // but it likely won't understand the rest of the message.
 | 
			
		||||
      //
 | 
			
		||||
      // We must send at least 3 bytes to be read, so we add
 | 
			
		||||
      // a message after the indicator byte to ensures its long
 | 
			
		||||
      // enough and can aid in debugging.
 | 
			
		||||
      const char msg[] = "\x00"
 | 
			
		||||
                         "Bad indicator byte";
 | 
			
		||||
      iov[0].iov_base = (void *) msg;
 | 
			
		||||
      iov[0].iov_len = 19;
 | 
			
		||||
      this->write_raw_(iov, 1);
 | 
			
		||||
    }
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame.msg);
 | 
			
		||||
  buffer->data_offset = 0;
 | 
			
		||||
@@ -1003,68 +902,128 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
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) {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding (frame_header_padding_ = 6)
 | 
			
		||||
  uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
 | 
			
		||||
  std::vector<uint8_t> header;
 | 
			
		||||
  header.push_back(0x00);
 | 
			
		||||
  ProtoVarInt(payload_len).encode(header);
 | 
			
		||||
  ProtoVarInt(type).encode(header);
 | 
			
		||||
 | 
			
		||||
  // Calculate varint sizes for header components
 | 
			
		||||
  uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
 | 
			
		||||
  uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
 | 
			
		||||
  uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
 | 
			
		||||
  struct iovec iov[2];
 | 
			
		||||
  iov[0].iov_base = &header[0];
 | 
			
		||||
  iov[0].iov_len = header.size();
 | 
			
		||||
  if (payload_len == 0) {
 | 
			
		||||
    return write_raw_(iov, 1);
 | 
			
		||||
  }
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(payload);
 | 
			
		||||
  iov[1].iov_len = payload_len;
 | 
			
		||||
 | 
			
		||||
  if (total_header_len > frame_header_padding_) {
 | 
			
		||||
    // Header is too large to fit in the padding
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (is_would_block(sent)) {
 | 
			
		||||
      break;
 | 
			
		||||
    } else if (sent == -1) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Calculate where to start writing the header
 | 
			
		||||
  // The header starts at the latest possible position to minimize unused padding
 | 
			
		||||
  //
 | 
			
		||||
  // Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
 | 
			
		||||
  // [0-2]  - Unused padding
 | 
			
		||||
  // [3]    - 0x00 indicator byte
 | 
			
		||||
  // [4]    - Payload size varint (1 byte, for sizes 0-127)
 | 
			
		||||
  // [5]    - Message type varint (1 byte, for types 0-127)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  //
 | 
			
		||||
  // Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
 | 
			
		||||
  // [0-1]  - Unused padding
 | 
			
		||||
  // [2]    - 0x00 indicator byte
 | 
			
		||||
  // [3-4]  - Payload size varint (2 bytes, for sizes 128-16383)
 | 
			
		||||
  // [5]    - Message type varint (1 byte, for types 0-127)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  //
 | 
			
		||||
  // Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
 | 
			
		||||
  // [0]    - 0x00 indicator byte
 | 
			
		||||
  // [1-3]  - Payload size varint (3 bytes, for sizes 16384-2097151)
 | 
			
		||||
  // [4-5]  - Message type varint (2 bytes, for types 128-32767)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  uint8_t *buf_start = raw_buffer->data();
 | 
			
		||||
  uint8_t header_offset = frame_header_padding_ - total_header_len;
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
  // Write the plaintext header
 | 
			
		||||
  buf_start[header_offset] = 0x00;  // indicator
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Encode size varint directly into buffer
 | 
			
		||||
  ProtoVarInt(payload_len).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Encode type varint directly into buffer
 | 
			
		||||
  ProtoVarInt(type).encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
  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() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -13,18 +12,22 @@
 | 
			
		||||
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
class ProtoWriteBuffer;
 | 
			
		||||
 | 
			
		||||
struct ReadPacketBuffer {
 | 
			
		||||
  std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint16_t data_offset;
 | 
			
		||||
  uint16_t data_len;
 | 
			
		||||
  size_t data_offset;
 | 
			
		||||
  size_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct PacketBuffer {
 | 
			
		||||
  const std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  uint8_t data_offset;
 | 
			
		||||
  uint8_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class APIError : int {
 | 
			
		||||
@@ -57,149 +60,63 @@ const char *api_error_to_str(APIError err);
 | 
			
		||||
 | 
			
		||||
class APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIFrameHelper() = default;
 | 
			
		||||
  explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
 | 
			
		||||
    socket_ = socket_owned_.get();
 | 
			
		||||
  }
 | 
			
		||||
  virtual ~APIFrameHelper() = default;
 | 
			
		||||
  virtual APIError init() = 0;
 | 
			
		||||
  virtual APIError loop() = 0;
 | 
			
		||||
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
 | 
			
		||||
  bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
  std::string getpeername() { return socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
 | 
			
		||||
  APIError close() {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
    int err = this->socket_->close();
 | 
			
		||||
    if (err == -1)
 | 
			
		||||
      return APIError::CLOSE_FAILED;
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  APIError shutdown(int how) {
 | 
			
		||||
    int err = this->socket_->shutdown(how);
 | 
			
		||||
    if (err == -1)
 | 
			
		||||
      return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
    if (how == SHUT_RDWR) {
 | 
			
		||||
      state_ = State::CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
  virtual bool can_write_without_blocking() = 0;
 | 
			
		||||
  virtual APIError write_packet(uint16_t type, const uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual std::string getpeername() = 0;
 | 
			
		||||
  virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
 | 
			
		||||
  virtual APIError close() = 0;
 | 
			
		||||
  virtual APIError shutdown(int how) = 0;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) { info_ = std::move(info); }
 | 
			
		||||
  virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  virtual uint8_t frame_header_padding() = 0;
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  virtual uint8_t frame_footer_size() = 0;
 | 
			
		||||
  // Check if socket has data ready to read
 | 
			
		||||
  bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Struct for holding parsed frame data
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Buffer containing data to be sent
 | 
			
		||||
  struct SendBuffer {
 | 
			
		||||
    std::vector<uint8_t> data;
 | 
			
		||||
    uint16_t offset{0};  // Current offset within the buffer (uint16_t to reduce memory usage)
 | 
			
		||||
 | 
			
		||||
    // Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
 | 
			
		||||
    uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
 | 
			
		||||
    const uint8_t *current_data() const { return data.data() + offset; }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Queue of data buffers to be sent
 | 
			
		||||
  std::deque<SendBuffer> tx_buf_;
 | 
			
		||||
 | 
			
		||||
  // Common state enum for all frame helpers
 | 
			
		||||
  // Note: Not all states are used by all implementations
 | 
			
		||||
  // - INITIALIZE: Used by both Noise and Plaintext
 | 
			
		||||
  // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
 | 
			
		||||
  // - DATA: Used by both Noise and Plaintext
 | 
			
		||||
  // - CLOSED: Used by both Noise and Plaintext
 | 
			
		||||
  // - FAILED: Used by both Noise and Plaintext
 | 
			
		||||
  // - EXPLICIT_REJECT: Only used by Noise protocol
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    CLIENT_HELLO = 2,  // Noise only
 | 
			
		||||
    SERVER_HELLO = 3,  // Noise only
 | 
			
		||||
    HANDSHAKE = 4,     // Noise only
 | 
			
		||||
    DATA = 5,
 | 
			
		||||
    CLOSED = 6,
 | 
			
		||||
    FAILED = 7,
 | 
			
		||||
    EXPLICIT_REJECT = 8,  // Noise only
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Current state of the frame helper
 | 
			
		||||
  State state_{State::INITIALIZE};
 | 
			
		||||
 | 
			
		||||
  // Helper name for logging
 | 
			
		||||
  std::string info_;
 | 
			
		||||
 | 
			
		||||
  // Socket for communication
 | 
			
		||||
  socket::Socket *socket_{nullptr};
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_owned_;
 | 
			
		||||
 | 
			
		||||
  // Common implementation for writing raw data to socket
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
 | 
			
		||||
  // Try to send data from the tx buffer
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
 | 
			
		||||
  // Helper method to buffer data from IOVs
 | 
			
		||||
  void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
 | 
			
		||||
  template<typename StateEnum>
 | 
			
		||||
  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};
 | 
			
		||||
 | 
			
		||||
  // Receive buffer for reading frame data
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  uint16_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  // Common initialization for both plaintext and noise protocols
 | 
			
		||||
  APIError init_common_();
 | 
			
		||||
  virtual void set_log_info(std::string info) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
 | 
			
		||||
      : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
 | 
			
		||||
    // Noise header structure:
 | 
			
		||||
    // Pos 0: indicator (0x01)
 | 
			
		||||
    // Pos 1-2: encrypted payload size (16-bit big-endian)
 | 
			
		||||
    // Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
 | 
			
		||||
    // Pos 7+: actual payload data
 | 
			
		||||
    frame_header_padding_ = 7;
 | 
			
		||||
  }
 | 
			
		||||
      : socket_(std::move(socket)), ctx_(std::move(std::move(ctx))) {}
 | 
			
		||||
  ~APINoiseFrameHelper() override;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
 | 
			
		||||
  // 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_; }
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  std::string getpeername() override { return this->socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
 | 
			
		||||
    return this->socket_->getpeername(addr, addrlen);
 | 
			
		||||
  }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, uint16_t len);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, size_t len);
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
  // 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 UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
 | 
			
		||||
  uint8_t rx_header_buf_[3];
 | 
			
		||||
  uint8_t rx_header_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  uint8_t rx_header_buf_[3];
 | 
			
		||||
  size_t rx_header_buf_len_ = 0;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
  std::vector<uint8_t> prologue_;
 | 
			
		||||
 | 
			
		||||
  std::shared_ptr<APINoiseContext> ctx_;
 | 
			
		||||
@@ -207,44 +124,67 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
  NoiseCipherState *send_cipher_{nullptr};
 | 
			
		||||
  NoiseCipherState *recv_cipher_{nullptr};
 | 
			
		||||
  NoiseProtocolId nid_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    CLIENT_HELLO = 2,
 | 
			
		||||
    SERVER_HELLO = 3,
 | 
			
		||||
    HANDSHAKE = 4,
 | 
			
		||||
    DATA = 5,
 | 
			
		||||
    CLOSED = 6,
 | 
			
		||||
    FAILED = 7,
 | 
			
		||||
    EXPLICIT_REJECT = 8,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
 | 
			
		||||
    // Plaintext header structure (worst case):
 | 
			
		||||
    // Pos 0: indicator (0x00)
 | 
			
		||||
    // Pos 1-3: payload size varint (up to 3 bytes)
 | 
			
		||||
    // Pos 4-5: message type varint (up to 2 bytes)
 | 
			
		||||
    // Pos 6+: actual payload data
 | 
			
		||||
    frame_header_padding_ = 6;
 | 
			
		||||
  }
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
 | 
			
		||||
  ~APIPlaintextFrameHelper() override = default;
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
 | 
			
		||||
  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_; }
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_packet(uint16_t type, const uint8_t *payload, size_t len) override;
 | 
			
		||||
  std::string getpeername() override { return this->socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
 | 
			
		||||
    return this->socket_->getpeername(addr, addrlen);
 | 
			
		||||
  }
 | 
			
		||||
  APIError close() override;
 | 
			
		||||
  APIError shutdown(int how) override;
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  void set_log_info(std::string info) override { info_ = std::move(info); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  // Fixed-size header buffer for plaintext protocol:
 | 
			
		||||
  // We now store the indicator byte + the two varints.
 | 
			
		||||
  // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
 | 
			
		||||
  // 1 byte for indicator + 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_[6];  // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type)
 | 
			
		||||
  uint8_t rx_header_buf_pos_ = 0;
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_raw_(const struct iovec *iov, int iovcnt);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  std::vector<uint8_t> rx_header_buf_;
 | 
			
		||||
  bool rx_header_parsed_ = false;
 | 
			
		||||
  uint16_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint16_t rx_header_parsed_len_ = 0;
 | 
			
		||||
  uint32_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint32_t rx_header_parsed_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
  size_t rx_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
 | 
			
		||||
  enum class State {
 | 
			
		||||
    INITIALIZE = 1,
 | 
			
		||||
    DATA = 2,
 | 
			
		||||
    CLOSED = 3,
 | 
			
		||||
    FAILED = 4,
 | 
			
		||||
  } state_ = State::INITIALIZE;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <array>
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -11,20 +11,11 @@ using psk_t = std::array<uint8_t, 32>;
 | 
			
		||||
 | 
			
		||||
class APINoiseContext {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_psk(psk_t psk) {
 | 
			
		||||
    this->psk_ = psk;
 | 
			
		||||
    bool has_psk = false;
 | 
			
		||||
    for (auto i : psk) {
 | 
			
		||||
      has_psk |= i;
 | 
			
		||||
    }
 | 
			
		||||
    this->has_psk_ = has_psk;
 | 
			
		||||
  }
 | 
			
		||||
  const psk_t &get_psk() const { return this->psk_; }
 | 
			
		||||
  bool has_psk() const { return this->has_psk_; }
 | 
			
		||||
  void set_psk(psk_t psk) { psk_ = psk; }
 | 
			
		||||
  const psk_t &get_psk() const { return psk_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  psk_t psk_{};
 | 
			
		||||
  bool has_psk_{false};
 | 
			
		||||
  psk_t psk_;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user