mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			131 Commits
		
	
	
		
			2025.5.0b4
			...
			add_api_st
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					c6858163a7 | ||
| 
						 | 
					d585440d54 | ||
| 
						 | 
					f74f89c6b5 | ||
| 
						 | 
					7d049a61bb | ||
| 
						 | 
					f2e4dc7907 | ||
| 
						 | 
					0c7589caeb | ||
| 
						 | 
					321411e355 | ||
| 
						 | 
					361de22370 | ||
| 
						 | 
					95a17387a8 | ||
| 
						 | 
					caf9930ff9 | ||
| 
						 | 
					42390faf4a | ||
| 
						 | 
					fdc6c4a219 | ||
| 
						 | 
					6c08f5e343 | ||
| 
						 | 
					e0e4ba9592 | ||
| 
						 | 
					ad20825f31 | ||
| 
						 | 
					e4f3a952d5 | ||
| 
						 | 
					90e3c5bba2 | ||
| 
						 | 
					b1d5ad27f3 | ||
| 
						 | 
					5c54f75b7a | ||
| 
						 | 
					a5f85b4437 | ||
| 
						 | 
					da4e710249 | ||
| 
						 | 
					4ac433fddb | ||
| 
						 | 
					73771d5c50 | ||
| 
						 | 
					af7b1a3a23 | ||
| 
						 | 
					430f63fcbb | ||
| 
						 | 
					5921a9cd68 | ||
| 
						 | 
					ca0037d076 | ||
| 
						 | 
					1e18d0b06c | ||
| 
						 | 
					4b5c3e7e2b | ||
| 
						 | 
					d4c4b75eb3 | ||
| 
						 | 
					9dd4045984 | ||
| 
						 | 
					19e2460af2 | ||
| 
						 | 
					149f787035 | ||
| 
						 | 
					0a1f3e813c | ||
| 
						 | 
					663f38d2ec | ||
| 
						 | 
					f0b311f839 | ||
| 
						 | 
					2ab1fe1abf | ||
| 
						 | 
					926b42ba1c | ||
| 
						 | 
					1c06137ae0 | ||
| 
						 | 
					377ed2e212 | ||
| 
						 | 
					42912447fb | ||
| 
						 | 
					25ead44f1c | ||
| 
						 | 
					03b003af47 | ||
| 
						 | 
					5baccf0ce7 | ||
| 
						 | 
					e95c92773c | ||
| 
						 | 
					c23ea384fb | ||
| 
						 | 
					69da17742f | ||
| 
						 | 
					1ec57a74b5 | ||
| 
						 | 
					d1e55252d0 | ||
| 
						 | 
					090feb55e9 | ||
| 
						 | 
					6109acb6f3 | ||
| 
						 | 
					5aa13db815 | ||
| 
						 | 
					1b67dd4232 | ||
| 
						 | 
					ba6efcedcb | ||
| 
						 | 
					bd7c2a680c | ||
| 
						 | 
					1466aa7703 | ||
| 
						 | 
					787f4860db | ||
| 
						 | 
					aeb4e63950 | ||
| 
						 | 
					026f47bfb3 | ||
| 
						 | 
					dd47d063b5 | ||
| 
						 | 
					cdcd1cd292 | ||
| 
						 | 
					a6fa963605 | ||
| 
						 | 
					1cba22175f | ||
| 
						 | 
					f2d7720a4e | ||
| 
						 | 
					801138da27 | ||
| 
						 | 
					51740a2e99 | ||
| 
						 | 
					d68a391e67 | ||
| 
						 | 
					e9d832d64a | ||
| 
						 | 
					f8f09bca02 | ||
| 
						 | 
					756aa13779 | ||
| 
						 | 
					25bbc0c221 | ||
| 
						 | 
					220a14e1f8 | ||
| 
						 | 
					ac74b25c46 | ||
| 
						 | 
					c5d809b3dd | ||
| 
						 | 
					b1cf08b261 | ||
| 
						 | 
					6ae83dfe3d | ||
| 
						 | 
					0932e83b15 | ||
| 
						 | 
					86670c4d39 | ||
| 
						 | 
					4ce55b94ec | ||
| 
						 | 
					1c5dc63eb4 | ||
| 
						 | 
					ef7a22ff04 | ||
| 
						 | 
					dfda0e5c7c | ||
| 
						 | 
					78c63311c6 | ||
| 
						 | 
					1ac51e7b3e | ||
| 
						 | 
					5b552b9ec5 | ||
| 
						 | 
					d36ce7c010 | ||
| 
						 | 
					b8a96f59f0 | ||
| 
						 | 
					2e15ee232d | ||
| 
						 | 
					904495e1b8 | ||
| 
						 | 
					99c4f88c3f | ||
| 
						 | 
					87a9dd18c8 | ||
| 
						 | 
					dbce54477a | ||
| 
						 | 
					660030d157 | ||
| 
						 | 
					24fbe602dd | ||
| 
						 | 
					b0c1e0e28c | ||
| 
						 | 
					574aabdede | ||
| 
						 | 
					e47741d471 | ||
| 
						 | 
					a78bea78f9 | ||
| 
						 | 
					44470f31f6 | ||
| 
						 | 
					18ac1b7c54 | ||
| 
						 | 
					e87b659483 | ||
| 
						 | 
					fefcb45e1f | ||
| 
						 | 
					ab415eb3de | ||
| 
						 | 
					5c92367ca2 | ||
| 
						 | 
					b469a504e4 | ||
| 
						 | 
					218f8e0caf | ||
| 
						 | 
					7965558d5e | ||
| 
						 | 
					d9b860088e | ||
| 
						 | 
					115975c409 | ||
| 
						 | 
					4761ffe023 | ||
| 
						 | 
					88edddf07a | ||
| 
						 | 
					0b77cb1d16 | ||
| 
						 | 
					efa6745a5e | ||
| 
						 | 
					dd8d8ad952 | ||
| 
						 | 
					57284b1ac3 | ||
| 
						 | 
					1a651ce66d | ||
| 
						 | 
					730441c120 | ||
| 
						 | 
					bb1f24ab43 | ||
| 
						 | 
					edb8d187be | ||
| 
						 | 
					e7b6081c5c | ||
| 
						 | 
					5454500024 | ||
| 
						 | 
					191afd3e69 | ||
| 
						 | 
					de27ce79dc | ||
| 
						 | 
					a12bd78ceb | ||
| 
						 | 
					ddb986b4fa | ||
| 
						 | 
					c98c78e368 | ||
| 
						 | 
					5570a788fd | ||
| 
						 | 
					42c355e6d7 | ||
| 
						 | 
					a835ab48bc | ||
| 
						 | 
					f28a373898 | ||
| 
						 | 
					28e29efd98 | 
@@ -1,2 +1,4 @@
 | 
			
		||||
[run]
 | 
			
		||||
omit = esphome/components/*
 | 
			
		||||
omit = 
 | 
			
		||||
    esphome/components/*
 | 
			
		||||
    tests/integration/*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										37
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
ARG BUILD_BASE_VERSION=2025.04.0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
 | 
			
		||||
 | 
			
		||||
RUN git config --system --add safe.directory "*"
 | 
			
		||||
 | 
			
		||||
RUN apt update \
 | 
			
		||||
    && apt install -y \
 | 
			
		||||
      protobuf-compiler
 | 
			
		||||
 | 
			
		||||
RUN pip install uv
 | 
			
		||||
 | 
			
		||||
RUN useradd esphome -m
 | 
			
		||||
 | 
			
		||||
USER esphome
 | 
			
		||||
ENV VIRTUAL_ENV=/home/esphome/.local/esphome-venv
 | 
			
		||||
RUN uv venv $VIRTUAL_ENV
 | 
			
		||||
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
 | 
			
		||||
# Override this set to true in the docker-base image
 | 
			
		||||
ENV UV_SYSTEM_PYTHON=false
 | 
			
		||||
 | 
			
		||||
WORKDIR /tmp
 | 
			
		||||
 | 
			
		||||
COPY requirements.txt ./
 | 
			
		||||
RUN uv pip install -r requirements.txt
 | 
			
		||||
COPY requirements_dev.txt requirements_test.txt ./
 | 
			
		||||
RUN uv pip install -r requirements_dev.txt -r requirements_test.txt
 | 
			
		||||
 | 
			
		||||
RUN \
 | 
			
		||||
    platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_platformio_interval 1000000
 | 
			
		||||
 | 
			
		||||
COPY script/platformio_install_deps.py platformio.ini ./
 | 
			
		||||
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
 | 
			
		||||
 | 
			
		||||
WORKDIR /workspaces
 | 
			
		||||
@@ -1,18 +1,17 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "ESPHome Dev",
 | 
			
		||||
  "image": "ghcr.io/esphome/esphome-lint:dev",
 | 
			
		||||
  "context": "..",
 | 
			
		||||
  "dockerFile": "Dockerfile",
 | 
			
		||||
  "postCreateCommand": [
 | 
			
		||||
    "script/devcontainer-post-create"
 | 
			
		||||
  ],
 | 
			
		||||
  "containerEnv": {
 | 
			
		||||
    "DEVCONTAINER": "1",
 | 
			
		||||
    "PIP_BREAK_SYSTEM_PACKAGES": "1",
 | 
			
		||||
    "PIP_ROOT_USER_ACTION": "ignore"
 | 
			
		||||
  "features": {
 | 
			
		||||
    "ghcr.io/devcontainers/features/github-cli:1": {}
 | 
			
		||||
  },
 | 
			
		||||
  "runArgs": [
 | 
			
		||||
    "--privileged",
 | 
			
		||||
    "-e",
 | 
			
		||||
    "ESPHOME_DASHBOARD_USE_PING=1"
 | 
			
		||||
    "GIT_EDITOR=code --wait"
 | 
			
		||||
    // uncomment and edit the path in order to pass though local USB serial to the conatiner
 | 
			
		||||
    // , "--device=/dev/ttyACM0"
 | 
			
		||||
  ],
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,7 +21,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,11 +43,11 @@ jobs:
 | 
			
		||||
          - "docker"
 | 
			
		||||
          # - "lint"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
          python-version: "3.10"
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										46
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,8 +20,8 @@ permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  DEFAULT_PYTHON: "3.9"
 | 
			
		||||
  PYUPGRADE_TARGET: "--py39-plus"
 | 
			
		||||
  DEFAULT_PYTHON: "3.10"
 | 
			
		||||
  PYUPGRADE_TARGET: "--py310-plus"
 | 
			
		||||
 | 
			
		||||
concurrency:
 | 
			
		||||
  # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -36,7 +36,7 @@ jobs:
 | 
			
		||||
      cache-key: ${{ steps.cache-key.outputs.key }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
@@ -68,7 +68,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -89,7 +89,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -110,7 +110,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -131,7 +131,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -152,7 +152,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -173,10 +173,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 +185,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.9"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
            os: macOS-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.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -214,14 +214,14 @@ jobs:
 | 
			
		||||
        if: matrix.os == 'windows-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          ./venv/Scripts/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native -n auto tests
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native -n auto tests
 | 
			
		||||
      - name: Upload coverage to Codecov
 | 
			
		||||
        uses: codecov/codecov-action@v5.4.2
 | 
			
		||||
        uses: codecov/codecov-action@v5.4.3
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
 | 
			
		||||
@@ -232,7 +232,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -300,7 +300,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -356,7 +356,7 @@ jobs:
 | 
			
		||||
      count: ${{ steps.list-components.outputs.count }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
 | 
			
		||||
          fetch-depth: 500
 | 
			
		||||
@@ -406,7 +406,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -432,7 +432,7 @@ jobs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Split components into 20 groups
 | 
			
		||||
        id: split
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -462,7 +462,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
      branch_build: ${{ steps.tag.outputs.branch_build }}
 | 
			
		||||
      deploy_env: ${{ steps.tag.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
        id: tag
 | 
			
		||||
        # yamllint disable rule:line-length
 | 
			
		||||
@@ -60,7 +60,7 @@ jobs:
 | 
			
		||||
      contents: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -92,11 +92,11 @@ jobs:
 | 
			
		||||
            os: "ubuntu-24.04-arm"
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.9"
 | 
			
		||||
          python-version: "3.10"
 | 
			
		||||
 | 
			
		||||
      - name: Set up Docker Buildx
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.10.0
 | 
			
		||||
@@ -168,7 +168,7 @@ jobs:
 | 
			
		||||
          - ghcr
 | 
			
		||||
          - dockerhub
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v4.1.7
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v4.3.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,10 +13,10 @@ jobs:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Checkout Home Assistant
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          repository: home-assistant/core
 | 
			
		||||
          path: lib/home-assistant
 | 
			
		||||
@@ -24,7 +24,7 @@ jobs:
 | 
			
		||||
      - name: Setup Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: 3.12
 | 
			
		||||
          python-version: 3.13
 | 
			
		||||
 | 
			
		||||
      - name: Install Home Assistant
 | 
			
		||||
        run: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Run yamllint
 | 
			
		||||
        uses: frenck/action-yamllint@v1.5.0
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.11.9
 | 
			
		||||
    rev: v0.11.10
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
@@ -28,10 +28,10 @@ repos:
 | 
			
		||||
          - --branch=release
 | 
			
		||||
          - --branch=beta
 | 
			
		||||
  - repo: https://github.com/asottile/pyupgrade
 | 
			
		||||
    rev: v3.15.2
 | 
			
		||||
    rev: v3.20.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: pyupgrade
 | 
			
		||||
        args: [--py39-plus]
 | 
			
		||||
        args: [--py310-plus]
 | 
			
		||||
  - repo: https://github.com/adrienverge/yamllint.git
 | 
			
		||||
    rev: v1.37.1
 | 
			
		||||
    hooks:
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,7 @@ 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
 | 
			
		||||
@@ -478,6 +479,8 @@ 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							@@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome
 | 
			
		||||
# could be handy for archiving the generated documentation or if some version
 | 
			
		||||
# control system is used.
 | 
			
		||||
 | 
			
		||||
PROJECT_NUMBER         = 2025.5.0b4
 | 
			
		||||
PROJECT_NUMBER         = 2025.6.0-dev
 | 
			
		||||
 | 
			
		||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
 | 
			
		||||
# for a project that appears at the top of each page and should give viewer a
 | 
			
		||||
 
 | 
			
		||||
@@ -432,7 +432,8 @@ message FanCommandRequest {
 | 
			
		||||
enum ColorMode {
 | 
			
		||||
  COLOR_MODE_UNKNOWN = 0;
 | 
			
		||||
  COLOR_MODE_ON_OFF = 1;
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 2;
 | 
			
		||||
  COLOR_MODE_LEGACY_BRIGHTNESS = 2;
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 3;
 | 
			
		||||
  COLOR_MODE_WHITE = 7;
 | 
			
		||||
  COLOR_MODE_COLOR_TEMPERATURE = 11;
 | 
			
		||||
  COLOR_MODE_COLD_WARM_WHITE = 19;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,15 @@
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_DEEP_SLEEP
 | 
			
		||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
 | 
			
		||||
@@ -85,6 +88,9 @@ void APIConnection::start() {
 | 
			
		||||
  // This ensures the first ping happens after the keepalive period
 | 
			
		||||
  this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
 | 
			
		||||
 | 
			
		||||
  // Pass stats collection to the helper for detailed timing
 | 
			
		||||
  this->helper_->set_section_stats(&this->section_stats_);
 | 
			
		||||
 | 
			
		||||
  APIError err = this->helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
@@ -111,6 +117,9 @@ APIConnection::~APIConnection() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::loop() {
 | 
			
		||||
  // Measure total time for entire loop function
 | 
			
		||||
  const uint32_t loop_start_time = millis();
 | 
			
		||||
 | 
			
		||||
  if (this->remove_)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
@@ -128,15 +137,30 @@ void APIConnection::loop() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  uint32_t start_time;
 | 
			
		||||
  uint32_t duration;
 | 
			
		||||
 | 
			
		||||
  // Section: Helper Loop
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["helper_loop"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
 | 
			
		||||
             api_error_to_str(err), errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Section: Read Packet
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  ReadPacketBuffer buffer;
 | 
			
		||||
  err = this->helper_->read_packet(&buffer);
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["read_packet"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK) {
 | 
			
		||||
    // pass
 | 
			
		||||
  } else if (err != APIError::OK) {
 | 
			
		||||
@@ -152,24 +176,42 @@ void APIConnection::loop() {
 | 
			
		||||
    return;
 | 
			
		||||
  } else {
 | 
			
		||||
    this->last_traffic_ = App.get_loop_component_start_time();
 | 
			
		||||
    // read a packet
 | 
			
		||||
    this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
 | 
			
		||||
    // Section: Process Message
 | 
			
		||||
    start_time = millis();
 | 
			
		||||
    if (buffer.data_len > 0) {
 | 
			
		||||
      this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->read_message(0, buffer.type, nullptr);
 | 
			
		||||
    }
 | 
			
		||||
    duration = millis() - start_time;
 | 
			
		||||
    this->section_stats_["process_message"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
    if (this->remove_)
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Section: Process Queue
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) {
 | 
			
		||||
    this->deferred_message_queue_.process_queue();
 | 
			
		||||
  }
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["process_queue"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  // Section: Iterator Advance
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  if (!this->list_entities_iterator_.completed())
 | 
			
		||||
    this->list_entities_iterator_.advance();
 | 
			
		||||
  if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
 | 
			
		||||
    this->initial_state_iterator_.advance();
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["iterator_advance"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  // Section: Keepalive
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  static uint8_t max_ping_retries = 60;
 | 
			
		||||
  static uint16_t ping_retry_interval = 1000;
 | 
			
		||||
  const uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (this->sent_ping_) {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
 | 
			
		||||
@@ -195,8 +237,12 @@ void APIConnection::loop() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["keepalive"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
  // Section: Camera
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
 | 
			
		||||
    // Message will use 8 more bytes than the minimum size, and typical
 | 
			
		||||
    // MTU is 1500. Sometimes users will see as low as 1460 MTU.
 | 
			
		||||
@@ -235,8 +281,12 @@ void APIConnection::loop() {
 | 
			
		||||
      this->image_reader_.return_image();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["camera"].record_time(duration);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Section: State Subscriptions
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  if (state_subs_at_ != -1) {
 | 
			
		||||
    const auto &subs = this->parent_->get_state_subs();
 | 
			
		||||
    if (state_subs_at_ >= (int) subs.size()) {
 | 
			
		||||
@@ -252,6 +302,24 @@ void APIConnection::loop() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["state_subs"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  // Log stats periodically
 | 
			
		||||
  if (this->stats_enabled_) {
 | 
			
		||||
    // If next_stats_log_ is 0, initialize it
 | 
			
		||||
    if (this->next_stats_log_ == 0) {
 | 
			
		||||
      this->next_stats_log_ = now + this->stats_log_interval_;
 | 
			
		||||
    } else if (now >= this->next_stats_log_) {
 | 
			
		||||
      this->log_section_stats_();
 | 
			
		||||
      this->reset_section_stats_();
 | 
			
		||||
      this->next_stats_log_ = now + this->stats_log_interval_;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Record total loop execution time
 | 
			
		||||
  const uint32_t total_loop_duration = millis() - loop_start_time;
 | 
			
		||||
  this->section_stats_["total_loop"].record_time(total_loop_duration);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
 | 
			
		||||
@@ -1628,8 +1696,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
 | 
			
		||||
    return false;
 | 
			
		||||
  if (this->helper_->can_write_without_blocking())
 | 
			
		||||
    return true;
 | 
			
		||||
 | 
			
		||||
  // Track try_to_clear_buffer time
 | 
			
		||||
  const uint32_t start_time = millis();
 | 
			
		||||
  delay(0);
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  const uint32_t duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["try_to_clear_buffer"].record_time(duration);
 | 
			
		||||
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
 | 
			
		||||
@@ -1644,11 +1718,17 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
 | 
			
		||||
  // Track send_buffer time
 | 
			
		||||
  const uint32_t start_time = millis();
 | 
			
		||||
 | 
			
		||||
  if (!this->try_to_clear_buffer(message_type != 29)) {  // SubscribeLogsResponse
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t write_start = millis();
 | 
			
		||||
  APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
 | 
			
		||||
  uint32_t write_duration = millis() - write_start;
 | 
			
		||||
  this->section_stats_["write_packet"].record_time(write_duration);
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
@@ -1661,6 +1741,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
 | 
			
		||||
    }
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Measure total send_buffer function time
 | 
			
		||||
  uint32_t total_duration = millis() - start_time;
 | 
			
		||||
  this->section_stats_["send_buffer_total"].record_time(total_duration);
 | 
			
		||||
 | 
			
		||||
  // Do not set last_traffic_ on send
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -1677,6 +1762,90 @@ void APIConnection::on_fatal_error() {
 | 
			
		||||
  this->remove_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::log_section_stats_() {
 | 
			
		||||
  const char *STATS_TAG = "api.stats";
 | 
			
		||||
  ESP_LOGI(STATS_TAG, "Logging API section stats now (current time: %" PRIu32 ", scheduled time: %" PRIu32 ")",
 | 
			
		||||
           millis(), this->next_stats_log_);
 | 
			
		||||
  ESP_LOGI(STATS_TAG, "Stats collection status: enabled=%d, sections=%zu", this->stats_enabled_,
 | 
			
		||||
           this->section_stats_.size());
 | 
			
		||||
 | 
			
		||||
  // Check if we have minimal data
 | 
			
		||||
  bool has_data = false;
 | 
			
		||||
  for (const auto &it : this->section_stats_) {
 | 
			
		||||
    if (it.second.get_period_count() > 0) {
 | 
			
		||||
      has_data = true;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (has_data) {
 | 
			
		||||
    size_t helper_count = 0;
 | 
			
		||||
    size_t read_count = 0;
 | 
			
		||||
    size_t total_count = 0;
 | 
			
		||||
    if (this->section_stats_.count("helper_loop") > 0)
 | 
			
		||||
      helper_count = this->section_stats_["helper_loop"].get_period_count();
 | 
			
		||||
    if (this->section_stats_.count("read_packet") > 0)
 | 
			
		||||
      read_count = this->section_stats_["read_packet"].get_period_count();
 | 
			
		||||
    if (this->section_stats_.count("total_loop") > 0)
 | 
			
		||||
      total_count = this->section_stats_["total_loop"].get_period_count();
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(STATS_TAG, "Record count for key sections: helper_loop=%zu, read_packet=%zu, total_loop=%zu", helper_count,
 | 
			
		||||
             read_count, total_count);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(STATS_TAG, "API Connection Section Runtime Statistics");
 | 
			
		||||
  ESP_LOGI(STATS_TAG, "Period stats (last %" PRIu32 "ms):", this->stats_log_interval_);
 | 
			
		||||
 | 
			
		||||
  // First collect stats we want to display
 | 
			
		||||
  std::vector<std::pair<std::string, const APISectionStats *>> stats_to_display;
 | 
			
		||||
 | 
			
		||||
  for (const auto &it : this->section_stats_) {
 | 
			
		||||
    const APISectionStats &stats = it.second;
 | 
			
		||||
    if (stats.get_period_count() > 0) {
 | 
			
		||||
      stats_to_display.push_back({it.first, &stats});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Sort by period runtime (descending)
 | 
			
		||||
  std::sort(stats_to_display.begin(), stats_to_display.end(), [](const auto &a, const auto &b) {
 | 
			
		||||
    return a.second->get_period_time_ms() > b.second->get_period_time_ms();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Log top components by period runtime
 | 
			
		||||
  for (const auto &it : stats_to_display) {
 | 
			
		||||
    const std::string §ion = it.first;
 | 
			
		||||
    const APISectionStats *stats = it.second;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(STATS_TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
 | 
			
		||||
             stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
 | 
			
		||||
             stats->get_period_time_ms());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Log total stats since boot
 | 
			
		||||
  ESP_LOGI(STATS_TAG, "Total stats (since boot):");
 | 
			
		||||
 | 
			
		||||
  // Re-sort by total runtime for all-time stats
 | 
			
		||||
  std::sort(stats_to_display.begin(), stats_to_display.end(),
 | 
			
		||||
            [](const auto &a, const auto &b) { return a.second->get_total_time_ms() > b.second->get_total_time_ms(); });
 | 
			
		||||
 | 
			
		||||
  for (const auto &it : stats_to_display) {
 | 
			
		||||
    const std::string §ion = it.first;
 | 
			
		||||
    const APISectionStats *stats = it.second;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(STATS_TAG, "  %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
 | 
			
		||||
             stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
 | 
			
		||||
             stats->get_total_time_ms());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(STATS_TAG, "Resetting API section stats, sections count: %zu", this->section_stats_.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::reset_section_stats_() {
 | 
			
		||||
  for (auto &it : this->section_stats_) {
 | 
			
		||||
    it.second.reset_period_stats();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -9,8 +9,12 @@
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
@@ -64,6 +68,9 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
 | 
			
		||||
  virtual ~APIConnection();
 | 
			
		||||
 | 
			
		||||
  // Use the APISectionStats from api_frame_helper.h to avoid duplication
 | 
			
		||||
  using APISectionStats = ::esphome::api::APISectionStats;
 | 
			
		||||
 | 
			
		||||
  void start();
 | 
			
		||||
  void loop();
 | 
			
		||||
 | 
			
		||||
@@ -556,6 +563,14 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  InitialStateIterator initial_state_iterator_;
 | 
			
		||||
  ListEntitiesIterator list_entities_iterator_;
 | 
			
		||||
  int state_subs_at_ = -1;
 | 
			
		||||
 | 
			
		||||
  // API loop section performance statistics
 | 
			
		||||
  std::map<std::string, APISectionStats> section_stats_;
 | 
			
		||||
  uint32_t stats_log_interval_{60000};  // 60 seconds default
 | 
			
		||||
  uint32_t next_stats_log_{0};
 | 
			
		||||
  bool stats_enabled_{true};
 | 
			
		||||
  void log_section_stats_();
 | 
			
		||||
  void reset_section_stats_();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -7,20 +7,13 @@
 | 
			
		||||
#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) {
 | 
			
		||||
@@ -73,92 +66,164 @@ const char *api_error_to_str(APIError err) {
 | 
			
		||||
  return "UNKNOWN";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for writing raw data to socket
 | 
			
		||||
template<typename StateEnum>
 | 
			
		||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
 | 
			
		||||
                                    std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
 | 
			
		||||
                                    StateEnum failed_state) {
 | 
			
		||||
  // This method writes data to socket or buffers it
 | 
			
		||||
// 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_state
 | 
			
		||||
  // Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
 | 
			
		||||
 | 
			
		||||
  if (iovcnt == 0)
 | 
			
		||||
    return APIError::OK;  // Nothing to do, success
 | 
			
		||||
 | 
			
		||||
  size_t total_write_len = 0;
 | 
			
		||||
  uint16_t total_write_len = 0;
 | 
			
		||||
  for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
#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;
 | 
			
		||||
    total_write_len += static_cast<uint16_t>(iov[i].iov_len);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf.empty()) {
 | 
			
		||||
    // try to empty tx_buf first
 | 
			
		||||
    while (!tx_buf.empty()) {
 | 
			
		||||
      ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
 | 
			
		||||
      if (is_would_block(sent)) {
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (sent == -1) {
 | 
			
		||||
        ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
 | 
			
		||||
        state = failed_state;
 | 
			
		||||
        return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
      }
 | 
			
		||||
      // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
      // replace with deque of buffers
 | 
			
		||||
      tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
 | 
			
		||||
  // 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
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!tx_buf.empty()) {
 | 
			
		||||
    // tx buf not empty, can't write now because then stream would be inconsistent
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + total_write_len);
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                    reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
  // Try to send directly if no buffered data
 | 
			
		||||
  uint32_t write_start = millis();
 | 
			
		||||
  ssize_t sent = this->socket_->writev(iov, iovcnt);
 | 
			
		||||
  uint32_t write_duration = millis() - write_start;
 | 
			
		||||
  if (write_duration > 0 && section_stats_) {
 | 
			
		||||
    (*section_stats_)["write_packet.socket_writev"].record_time(write_duration);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ssize_t sent = socket->writev(iov, iovcnt);
 | 
			
		||||
  if (is_would_block(sent)) {
 | 
			
		||||
    // operation would block, add buffer to tx_buf
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + total_write_len);
 | 
			
		||||
    for (int i = 0; i < iovcnt; i++) {
 | 
			
		||||
      tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
 | 
			
		||||
                    reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
 | 
			
		||||
  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
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
  } else if (sent == -1) {
 | 
			
		||||
    // an error occurred
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
 | 
			
		||||
    state = failed_state;
 | 
			
		||||
    // 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 ((size_t) sent != total_write_len) {
 | 
			
		||||
    // partially sent, add end to tx_buf
 | 
			
		||||
    size_t remaining = total_write_len - sent;
 | 
			
		||||
    // Reserve space upfront to avoid multiple reallocations
 | 
			
		||||
    tx_buf.reserve(tx_buf.size() + remaining);
 | 
			
		||||
  } 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);
 | 
			
		||||
 | 
			
		||||
    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;
 | 
			
		||||
        // This segment was fully sent
 | 
			
		||||
        to_consume -= static_cast<uint16_t>(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);
 | 
			
		||||
        // 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;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return APIError::OK;  // Success, data buffered
 | 
			
		||||
 | 
			
		||||
    this->tx_buf_.push_back(std::move(buffer));
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;  // Success, all data sent
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;  // Success, all data sent or buffered
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// Common implementation for trying to send buffered data
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
 | 
			
		||||
APIError APIFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
 | 
			
		||||
  bool tx_buf_empty = false;
 | 
			
		||||
  while (!tx_buf_empty) {
 | 
			
		||||
    // Get the first buffer in the queue
 | 
			
		||||
    SendBuffer &front_buffer = this->tx_buf_.front();
 | 
			
		||||
 | 
			
		||||
    // Try to send the remaining data in this buffer
 | 
			
		||||
    uint32_t write_start = millis();
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
 | 
			
		||||
    uint32_t write_duration = millis() - write_start;
 | 
			
		||||
    if (write_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["send_buffer_total.socket_write"].record_time(write_duration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno != EWOULDBLOCK && errno != EAGAIN) {
 | 
			
		||||
        // Real socket error (not just would block)
 | 
			
		||||
        ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
        this->state_ = State::FAILED;
 | 
			
		||||
        return APIError::SOCKET_WRITE_FAILED;  // Socket write failed
 | 
			
		||||
      }
 | 
			
		||||
      // Socket would block, we'll try again later
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      // Nothing sent but not an error
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
 | 
			
		||||
      // Partially sent, update offset
 | 
			
		||||
      // Cast to ensure no overflow issues with uint16_t
 | 
			
		||||
      front_buffer.offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      return APIError::WOULD_BLOCK;  // Stop processing more buffers if we couldn't send a complete buffer
 | 
			
		||||
    } else {
 | 
			
		||||
      // Buffer completely sent, remove it from the queue
 | 
			
		||||
      this->tx_buf_.pop_front();
 | 
			
		||||
      // Update empty status for the loop condition
 | 
			
		||||
      tx_buf_empty = this->tx_buf_.empty();
 | 
			
		||||
      // Continue loop to try sending the next buffer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;  // All buffers sent successfully
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIFrameHelper::init_common_() {
 | 
			
		||||
  if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_);
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  int err = this->socket_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
    return APIError::TCP_NONBLOCKING_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno);
 | 
			
		||||
    return APIError::TCP_NODELAY_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
@@ -206,23 +271,9 @@ std::string noise_err_to_str(int err) {
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APINoiseFrameHelper::init() {
 | 
			
		||||
  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;
 | 
			
		||||
  APIError err = init_common_();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // init prologue
 | 
			
		||||
@@ -234,17 +285,16 @@ APIError APINoiseFrameHelper::init() {
 | 
			
		||||
/// Run through handshake messages (if in that phase)
 | 
			
		||||
APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  APIError err = state_action_();
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  if (err != APIError::OK)
 | 
			
		||||
  if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
    err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
  return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
@@ -270,8 +320,13 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
    size_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    uint8_t to_read = 3 - rx_header_buf_len_;
 | 
			
		||||
    uint32_t socket_start = millis();
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
 | 
			
		||||
    uint32_t socket_duration = millis() - socket_start;
 | 
			
		||||
    if (socket_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.socket_read_header"].record_time(socket_duration);
 | 
			
		||||
    }
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -284,8 +339,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
    rx_header_buf_len_ += static_cast<uint8_t>(received);
 | 
			
		||||
    if (static_cast<uint8_t>(received) != to_read) {
 | 
			
		||||
      // not a full read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -312,13 +367,23 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != msg_size) {
 | 
			
		||||
    uint32_t resize_start = millis();
 | 
			
		||||
    rx_buf_.resize(msg_size);
 | 
			
		||||
    uint32_t resize_duration = millis() - resize_start;
 | 
			
		||||
    if (resize_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.buffer_resize"].record_time(resize_duration);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
    // more data to read
 | 
			
		||||
    size_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    uint16_t to_read = msg_size - rx_buf_len_;
 | 
			
		||||
    uint32_t socket_start = millis();
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    uint32_t socket_duration = millis() - socket_start;
 | 
			
		||||
    if (socket_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.socket_read_body"].record_time(socket_duration);
 | 
			
		||||
    }
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -331,8 +396,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -381,6 +446,8 @@ 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());
 | 
			
		||||
@@ -389,16 +456,20 @@ 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 std::string &mac = get_mac_address();
 | 
			
		||||
    const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
 | 
			
		||||
    msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
 | 
			
		||||
 | 
			
		||||
@@ -505,11 +576,18 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
 | 
			
		||||
  write_frame_(data.data(), data.size());
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  uint32_t start_time, duration;
 | 
			
		||||
 | 
			
		||||
  // Track state_action timing
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  if (duration > 0 && section_stats_) {
 | 
			
		||||
    (*section_stats_)["read_packet.state_action"].record_time(duration);
 | 
			
		||||
  }
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
@@ -518,22 +596,34 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Track frame reading timing
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  if (duration > 0 && section_stats_) {
 | 
			
		||||
    (*section_stats_)["read_packet.try_read_frame"].record_time(duration);
 | 
			
		||||
  }
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  // Track decryption timing
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
 | 
			
		||||
  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  if (duration > 0 && section_stats_) {
 | 
			
		||||
    (*section_stats_)["read_packet.decrypt"].record_time(duration);
 | 
			
		||||
  }
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
 | 
			
		||||
    return APIError::CIPHERSTATE_DECRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t msg_size = mbuf.size;
 | 
			
		||||
  uint16_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.msg.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
@@ -559,7 +649,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
@@ -574,9 +663,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding
 | 
			
		||||
  size_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
  size_t padding = 0;
 | 
			
		||||
  size_t msg_len = 4 + payload_len + padding;
 | 
			
		||||
  uint16_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
  uint16_t padding = 0;
 | 
			
		||||
  uint16_t msg_len = 4 + payload_len + padding;
 | 
			
		||||
 | 
			
		||||
  // 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_);
 | 
			
		||||
@@ -609,7 +698,7 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
 | 
			
		||||
    return APIError::CIPHERSTATE_ENCRYPT_FAILED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t total_len = 3 + mbuf.size;
 | 
			
		||||
  uint16_t total_len = 3 + mbuf.size;
 | 
			
		||||
  buf_start[1] = (uint8_t) (mbuf.size >> 8);
 | 
			
		||||
  buf_start[2] = (uint8_t) mbuf.size;
 | 
			
		||||
 | 
			
		||||
@@ -620,29 +709,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
 | 
			
		||||
  iov.iov_len = total_len;
 | 
			
		||||
 | 
			
		||||
  // write raw to not have two packets sent if NAGLE disabled
 | 
			
		||||
  return write_raw_(&iov, 1);
 | 
			
		||||
  return this->write_raw_(&iov, 1);
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // try send from tx_buf
 | 
			
		||||
  while (state_ != State::CLOSED && !tx_buf_.empty()) {
 | 
			
		||||
    ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN)
 | 
			
		||||
        break;
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Socket write failed with errno %d", errno);
 | 
			
		||||
      return APIError::SOCKET_WRITE_FAILED;
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: inefficient if multiple packets in txbuf
 | 
			
		||||
    // replace with deque of buffers
 | 
			
		||||
    tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
 | 
			
		||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
 | 
			
		||||
  uint8_t header[3];
 | 
			
		||||
  header[0] = 0x01;  // indicator
 | 
			
		||||
  header[1] = (uint8_t) (len >> 8);
 | 
			
		||||
@@ -652,12 +721,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
 | 
			
		||||
  iov[0].iov_base = header;
 | 
			
		||||
  iov[0].iov_len = 3;
 | 
			
		||||
  if (len == 0) {
 | 
			
		||||
    return write_raw_(iov, 1);
 | 
			
		||||
    return this->write_raw_(iov, 1);
 | 
			
		||||
  }
 | 
			
		||||
  iov[1].iov_base = const_cast<uint8_t *>(data);
 | 
			
		||||
  iov[1].iov_len = len;
 | 
			
		||||
 | 
			
		||||
  return write_raw_(iov, 2);
 | 
			
		||||
  return this->write_raw_(iov, 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Initiate the data structures for the handshake.
 | 
			
		||||
@@ -752,22 +821,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::close() {
 | 
			
		||||
  state_ = State::CLOSED;
 | 
			
		||||
  int err = socket_->close();
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::CLOSE_FAILED;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::shutdown(int how) {
 | 
			
		||||
  int err = socket_->shutdown(how);
 | 
			
		||||
  if (err == -1)
 | 
			
		||||
    return APIError::SHUTDOWN_FAILED;
 | 
			
		||||
  if (how == SHUT_RDWR) {
 | 
			
		||||
    state_ = State::CLOSED;
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
extern "C" {
 | 
			
		||||
// 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) {
 | 
			
		||||
@@ -778,32 +831,15 @@ void noise_rand_bytes(void *output, size_t len) {
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiation for Noise
 | 
			
		||||
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
 | 
			
		||||
    const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
 | 
			
		||||
    APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
/// Initialize the frame helper, returns OK if successful.
 | 
			
		||||
APIError APIPlaintextFrameHelper::init() {
 | 
			
		||||
  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;
 | 
			
		||||
  APIError err = init_common_();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  state_ = State::DATA;
 | 
			
		||||
@@ -814,14 +850,13 @@ APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
  }
 | 
			
		||||
  // try send pending TX data
 | 
			
		||||
  if (!tx_buf_.empty()) {
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK) {
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
  return APIError::OK;  // Convert WOULD_BLOCK to OK to avoid connection termination
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
@@ -846,7 +881,12 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    // there is no data on the wire (which is the common case).
 | 
			
		||||
    // This results in faster failure detection compared to
 | 
			
		||||
    // attempting to read multiple bytes at once.
 | 
			
		||||
    ssize_t received = socket_->read(&data, 1);
 | 
			
		||||
    uint32_t socket_start = millis();
 | 
			
		||||
    ssize_t received = this->socket_->read(&data, 1);
 | 
			
		||||
    uint32_t socket_duration = millis() - socket_start;
 | 
			
		||||
    if (socket_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.socket_read_header"].record_time(socket_duration);
 | 
			
		||||
    }
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -897,7 +937,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
    //   - At least 2 bytes in the buffer for the varints
 | 
			
		||||
    // Buffer layout:
 | 
			
		||||
    //   First 1-3 bytes: Message size varint (variable length)
 | 
			
		||||
    //     - 2 bytes would only allow up to 16383, which is less than noise's 65535
 | 
			
		||||
    //     - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
 | 
			
		||||
    //     - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
 | 
			
		||||
    //   Remaining 1-2 bytes: Message type varint (variable length)
 | 
			
		||||
    // We now attempt to parse both varints. If either is incomplete,
 | 
			
		||||
@@ -910,27 +950,49 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint32();
 | 
			
		||||
    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();
 | 
			
		||||
 | 
			
		||||
    auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
 | 
			
		||||
    if (!msg_type_varint.has_value()) {
 | 
			
		||||
      // not enough data there yet
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint32();
 | 
			
		||||
    if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
 | 
			
		||||
                 std::numeric_limits<uint16_t>::max());
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_type_ = msg_type_varint->as_uint16();
 | 
			
		||||
    rx_header_parsed_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != rx_header_parsed_len_) {
 | 
			
		||||
    uint32_t resize_start = millis();
 | 
			
		||||
    rx_buf_.resize(rx_header_parsed_len_);
 | 
			
		||||
    uint32_t resize_duration = millis() - resize_start;
 | 
			
		||||
    if (resize_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.buffer_resize"].record_time(resize_duration);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
    // more data 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);
 | 
			
		||||
    uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
 | 
			
		||||
    uint32_t socket_start = millis();
 | 
			
		||||
    ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
 | 
			
		||||
    uint32_t socket_duration = millis() - socket_start;
 | 
			
		||||
    if (socket_duration > 0 && section_stats_) {
 | 
			
		||||
      (*section_stats_)["read_packet.socket_read_body"].record_time(socket_duration);
 | 
			
		||||
    }
 | 
			
		||||
    if (received == -1) {
 | 
			
		||||
      if (errno == EWOULDBLOCK || errno == EAGAIN) {
 | 
			
		||||
        return APIError::WOULD_BLOCK;
 | 
			
		||||
@@ -943,8 +1005,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
      HELPER_LOG("Connection closed");
 | 
			
		||||
      return APIError::CONNECTION_CLOSED;
 | 
			
		||||
    }
 | 
			
		||||
    rx_buf_len_ += received;
 | 
			
		||||
    if ((size_t) received != to_read) {
 | 
			
		||||
    rx_buf_len_ += static_cast<uint16_t>(received);
 | 
			
		||||
    if (static_cast<uint16_t>(received) != to_read) {
 | 
			
		||||
      // not all read
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    }
 | 
			
		||||
@@ -962,16 +1024,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  uint32_t start_time, duration;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Track frame reading timing
 | 
			
		||||
  start_time = millis();
 | 
			
		||||
  ParsedFrame frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  duration = millis() - start_time;
 | 
			
		||||
  if (duration > 0 && section_stats_) {
 | 
			
		||||
    (*section_stats_)["read_packet.try_read_frame"].record_time(duration);
 | 
			
		||||
  }
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      // Make sure to tell the remote that we don't
 | 
			
		||||
@@ -990,7 +1058,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
                         "Bad indicator byte";
 | 
			
		||||
      iov[0].iov_base = (void *) msg;
 | 
			
		||||
      iov[0].iov_len = 19;
 | 
			
		||||
      write_raw_(iov, 1);
 | 
			
		||||
      this->write_raw_(iov, 1);
 | 
			
		||||
    }
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
@@ -1001,7 +1069,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
    return APIError::BAD_STATE;
 | 
			
		||||
@@ -1009,12 +1076,12 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  // Message data starts after padding (frame_header_padding_ = 6)
 | 
			
		||||
  size_t payload_len = raw_buffer->size() - frame_header_padding_;
 | 
			
		||||
  uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
 | 
			
		||||
 | 
			
		||||
  // Calculate varint sizes for header components
 | 
			
		||||
  size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
 | 
			
		||||
  size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
 | 
			
		||||
  size_t total_header_len = 1 + size_varint_len + type_varint_len;
 | 
			
		||||
  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;
 | 
			
		||||
 | 
			
		||||
  if (total_header_len > frame_header_padding_) {
 | 
			
		||||
    // Header is too large to fit in the padding
 | 
			
		||||
@@ -1044,7 +1111,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
 | 
			
		||||
  // [4-5]  - Message type varint (2 bytes, for types 128-32767)
 | 
			
		||||
  // [6...] - Actual payload data
 | 
			
		||||
  uint8_t *buf_start = raw_buffer->data();
 | 
			
		||||
  size_t header_offset = frame_header_padding_ - total_header_len;
 | 
			
		||||
  uint8_t header_offset = frame_header_padding_ - total_header_len;
 | 
			
		||||
 | 
			
		||||
  // Write the plaintext header
 | 
			
		||||
  buf_start[header_offset] = 0x00;  // indicator
 | 
			
		||||
@@ -1063,46 +1130,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
 | 
			
		||||
 | 
			
		||||
  return write_raw_(&iov, 1);
 | 
			
		||||
}
 | 
			
		||||
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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Explicit template instantiation for Plaintext
 | 
			
		||||
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
 | 
			
		||||
    const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
 | 
			
		||||
    APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
 | 
			
		||||
#endif  // USE_API_PLAINTEXT
 | 
			
		||||
 | 
			
		||||
}  // namespace api
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -12,24 +13,78 @@
 | 
			
		||||
 | 
			
		||||
#include "api_noise_context.h"
 | 
			
		||||
#include "esphome/components/socket/socket.h"
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace api {
 | 
			
		||||
 | 
			
		||||
// Forward declaration from api_connection.h
 | 
			
		||||
class APIConnection;
 | 
			
		||||
 | 
			
		||||
// Stats class definition (copied from api_connection.h to avoid circular dependency)
 | 
			
		||||
class APISectionStats {
 | 
			
		||||
 public:
 | 
			
		||||
  APISectionStats()
 | 
			
		||||
      : period_count_(0),
 | 
			
		||||
        total_count_(0),
 | 
			
		||||
        period_time_ms_(0),
 | 
			
		||||
        total_time_ms_(0),
 | 
			
		||||
        period_max_time_ms_(0),
 | 
			
		||||
        total_max_time_ms_(0) {}
 | 
			
		||||
 | 
			
		||||
  void record_time(uint32_t duration_ms) {
 | 
			
		||||
    // Update period counters
 | 
			
		||||
    this->period_count_++;
 | 
			
		||||
    this->period_time_ms_ += duration_ms;
 | 
			
		||||
    if (duration_ms > this->period_max_time_ms_)
 | 
			
		||||
      this->period_max_time_ms_ = duration_ms;
 | 
			
		||||
 | 
			
		||||
    // Update total counters
 | 
			
		||||
    this->total_count_++;
 | 
			
		||||
    this->total_time_ms_ += duration_ms;
 | 
			
		||||
    if (duration_ms > this->total_max_time_ms_)
 | 
			
		||||
      this->total_max_time_ms_ = duration_ms;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void reset_period_stats() {
 | 
			
		||||
    this->period_count_ = 0;
 | 
			
		||||
    this->period_time_ms_ = 0;
 | 
			
		||||
    this->period_max_time_ms_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Getters for period stats
 | 
			
		||||
  uint32_t get_period_count() const { return this->period_count_; }
 | 
			
		||||
  uint32_t get_period_time_ms() const { return this->period_time_ms_; }
 | 
			
		||||
  uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
 | 
			
		||||
  float get_period_avg_time_ms() const {
 | 
			
		||||
    return this->period_count_ > 0 ? static_cast<float>(this->period_time_ms_) / this->period_count_ : 0.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Getters for total stats
 | 
			
		||||
  uint32_t get_total_count() const { return this->total_count_; }
 | 
			
		||||
  uint32_t get_total_time_ms() const { return this->total_time_ms_; }
 | 
			
		||||
  uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
 | 
			
		||||
  float get_total_avg_time_ms() const {
 | 
			
		||||
    return this->total_count_ > 0 ? static_cast<float>(this->total_time_ms_) / this->total_count_ : 0.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  uint32_t period_count_;
 | 
			
		||||
  uint32_t total_count_;
 | 
			
		||||
  uint32_t period_time_ms_;
 | 
			
		||||
  uint32_t total_time_ms_;
 | 
			
		||||
  uint32_t period_max_time_ms_;
 | 
			
		||||
  uint32_t total_max_time_ms_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ProtoWriteBuffer;
 | 
			
		||||
 | 
			
		||||
struct ReadPacketBuffer {
 | 
			
		||||
  std::vector<uint8_t> container;
 | 
			
		||||
  uint16_t type;
 | 
			
		||||
  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;
 | 
			
		||||
  uint16_t data_offset;
 | 
			
		||||
  uint16_t data_len;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class APIError : int {
 | 
			
		||||
@@ -62,38 +117,122 @@ 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;
 | 
			
		||||
  virtual bool can_write_without_blocking() = 0;
 | 
			
		||||
  virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 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;
 | 
			
		||||
  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;
 | 
			
		||||
  }
 | 
			
		||||
  // Give this helper a name for logging
 | 
			
		||||
  virtual void set_log_info(std::string info) = 0;
 | 
			
		||||
  void set_log_info(std::string info) { info_ = std::move(info); }
 | 
			
		||||
  // Set stats collection for detailed timing
 | 
			
		||||
  void set_section_stats(std::map<std::string, APISectionStats> *stats) { section_stats_ = stats; }
 | 
			
		||||
  virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  virtual uint8_t frame_header_padding() = 0;
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  virtual uint8_t frame_footer_size() = 0;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // 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_();
 | 
			
		||||
 | 
			
		||||
  // Stats collection pointer - shared from APIConnection
 | 
			
		||||
  std::map<std::string, APISectionStats> *section_stats_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
 public:
 | 
			
		||||
  APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
 | 
			
		||||
      : socket_(std::move(socket)), ctx_(std::move(ctx)) {
 | 
			
		||||
      : APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
 | 
			
		||||
    // Noise header structure:
 | 
			
		||||
    // Pos 0: indicator (0x01)
 | 
			
		||||
    // Pos 1-2: encrypted payload size (16-bit big-endian)
 | 
			
		||||
@@ -105,49 +244,25 @@ class APINoiseFrameHelper : public APIFrameHelper {
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) 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); }
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  uint8_t frame_header_padding() override { return frame_header_padding_; }
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  uint8_t frame_footer_size() override { return frame_footer_size_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, size_t len);
 | 
			
		||||
  inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
    return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
 | 
			
		||||
  }
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, uint16_t len);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
  void send_explicit_handshake_reject_(const std::string &reason);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  // Fixed-size header buffer for noise protocol:
 | 
			
		||||
  // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
 | 
			
		||||
  // Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
 | 
			
		||||
  // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
 | 
			
		||||
  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;
 | 
			
		||||
  uint8_t rx_header_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> tx_buf_;
 | 
			
		||||
  std::vector<uint8_t> prologue_;
 | 
			
		||||
 | 
			
		||||
  std::shared_ptr<APINoiseContext> ctx_;
 | 
			
		||||
@@ -155,24 +270,13 @@ 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) : socket_(std::move(socket)) {
 | 
			
		||||
  APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
 | 
			
		||||
    // Plaintext header structure (worst case):
 | 
			
		||||
    // Pos 0: indicator (0x00)
 | 
			
		||||
    // Pos 1-3: payload size varint (up to 3 bytes)
 | 
			
		||||
@@ -184,38 +288,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
  APIError init() override;
 | 
			
		||||
  APIError loop() override;
 | 
			
		||||
  APIError read_packet(ReadPacketBuffer *buffer) override;
 | 
			
		||||
  bool can_write_without_blocking() override;
 | 
			
		||||
  APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) 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); }
 | 
			
		||||
  // Get the frame header padding required by this protocol
 | 
			
		||||
  uint8_t frame_header_padding() override { return frame_header_padding_; }
 | 
			
		||||
  // Get the frame footer size required by this protocol
 | 
			
		||||
  uint8_t frame_footer_size() override { return frame_footer_size_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  struct ParsedFrame {
 | 
			
		||||
    std::vector<uint8_t> msg;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  APIError try_read_frame_(ParsedFrame *frame);
 | 
			
		||||
  APIError try_send_tx_buf_();
 | 
			
		||||
  inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
 | 
			
		||||
    return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
 | 
			
		||||
  std::string info_;
 | 
			
		||||
  // Fixed-size header buffer for plaintext protocol:
 | 
			
		||||
  // We only need space for the two varints since we validate the indicator byte separately.
 | 
			
		||||
  // To match noise protocol's maximum message size (65535), we need:
 | 
			
		||||
  // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
 | 
			
		||||
  // 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
 | 
			
		||||
  //
 | 
			
		||||
  // While varints could theoretically be up to 10 bytes each for 64-bit values,
 | 
			
		||||
@@ -224,20 +306,8 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
 | 
			
		||||
  uint8_t rx_header_buf_[5];  // 5 bytes for varints (3 for size + 2 for type)
 | 
			
		||||
  uint8_t rx_header_buf_pos_ = 0;
 | 
			
		||||
  bool rx_header_parsed_ = false;
 | 
			
		||||
  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;
 | 
			
		||||
  uint16_t rx_header_parsed_type_ = 0;
 | 
			
		||||
  uint16_t rx_header_parsed_len_ = 0;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,8 @@ template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode v
 | 
			
		||||
      return "COLOR_MODE_UNKNOWN";
 | 
			
		||||
    case enums::COLOR_MODE_ON_OFF:
 | 
			
		||||
      return "COLOR_MODE_ON_OFF";
 | 
			
		||||
    case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
 | 
			
		||||
      return "COLOR_MODE_LEGACY_BRIGHTNESS";
 | 
			
		||||
    case enums::COLOR_MODE_BRIGHTNESS:
 | 
			
		||||
      return "COLOR_MODE_BRIGHTNESS";
 | 
			
		||||
    case enums::COLOR_MODE_WHITE:
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,8 @@ enum FanDirection : uint32_t {
 | 
			
		||||
enum ColorMode : uint32_t {
 | 
			
		||||
  COLOR_MODE_UNKNOWN = 0,
 | 
			
		||||
  COLOR_MODE_ON_OFF = 1,
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 2,
 | 
			
		||||
  COLOR_MODE_LEGACY_BRIGHTNESS = 2,
 | 
			
		||||
  COLOR_MODE_BRIGHTNESS = 3,
 | 
			
		||||
  COLOR_MODE_WHITE = 7,
 | 
			
		||||
  COLOR_MODE_COLOR_TEMPERATURE = 11,
 | 
			
		||||
  COLOR_MODE_COLD_WARM_WHITE = 19,
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,7 @@ class ProtoVarInt {
 | 
			
		||||
    return {};  // Incomplete or invalid varint
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t as_uint16() const { return this->value_; }
 | 
			
		||||
  uint32_t as_uint32() const { return this->value_; }
 | 
			
		||||
  uint64_t as_uint64() const { return this->value_; }
 | 
			
		||||
  bool as_bool() const { return this->value_; }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,11 +14,8 @@ namespace esphome {
 | 
			
		||||
namespace at581x {
 | 
			
		||||
 | 
			
		||||
class AT581XComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
 protected:
 | 
			
		||||
  switch_::Switch *rf_power_switch_{nullptr};
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  void set_rf_power_switch(switch_::Switch *s) {
 | 
			
		||||
    this->rf_power_switch_ = s;
 | 
			
		||||
    s->turn_on();
 | 
			
		||||
@@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
 | 
			
		||||
  bool i2c_read_reg(uint8_t addr, uint8_t &data);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  switch_::Switch *rf_power_switch_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  int freq_;
 | 
			
		||||
  int self_check_time_ms_;   /*!< Power-on self-test time, range: 0 ~ 65536 ms */
 | 
			
		||||
  int protect_time_ms_;      /*!< Protection time, recommended 1000 ms */
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LINE_FREQUENCY,
 | 
			
		||||
    CONF_POWER,
 | 
			
		||||
    CONF_RESET,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    DEVICE_CLASS_CURRENT,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
@@ -27,7 +28,6 @@ from esphome.const import (
 | 
			
		||||
CONF_CURRENT_REFERENCE = "current_reference"
 | 
			
		||||
CONF_ENERGY_REFERENCE = "energy_reference"
 | 
			
		||||
CONF_POWER_REFERENCE = "power_reference"
 | 
			
		||||
CONF_RESET = "reset"
 | 
			
		||||
CONF_VOLTAGE_REFERENCE = "voltage_reference"
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
 | 
			
		||||
 | 
			
		||||
DOMAIN = "bme68x_bsec2"
 | 
			
		||||
 | 
			
		||||
BSEC2_LIBRARY_VERSION = "v1.8.2610"
 | 
			
		||||
BSEC2_LIBRARY_VERSION = "1.10.2610"
 | 
			
		||||
 | 
			
		||||
CONF_ALGORITHM_OUTPUT = "algorithm_output"
 | 
			
		||||
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
 | 
			
		||||
@@ -145,7 +145,6 @@ CONFIG_SCHEMA_BASE = (
 | 
			
		||||
            ): cv.positive_time_period_minutes,
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .add_extra(cv.only_with_arduino)
 | 
			
		||||
    .add_extra(validate_bme68x)
 | 
			
		||||
    .add_extra(download_bme68x_blob)
 | 
			
		||||
)
 | 
			
		||||
@@ -179,11 +178,13 @@ async def to_code_base(config):
 | 
			
		||||
    bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
 | 
			
		||||
    cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
 | 
			
		||||
 | 
			
		||||
    # Although this component does not use SPI, the BSEC2 library requires the SPI library
 | 
			
		||||
    cg.add_library("SPI", None)
 | 
			
		||||
    # Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library
 | 
			
		||||
    if core.CORE.using_arduino:
 | 
			
		||||
        cg.add_library("SPI", None)
 | 
			
		||||
    cg.add_library(
 | 
			
		||||
        "BME68x Sensor library",
 | 
			
		||||
        "1.1.40407",
 | 
			
		||||
        "1.3.40408",
 | 
			
		||||
        "https://github.com/boschsensortec/Bosch-BME68x-Library",
 | 
			
		||||
    )
 | 
			
		||||
    cg.add_library(
 | 
			
		||||
        "BSEC2 Software Library",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/cm1106/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/cm1106/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
"""CM1106 component for ESPHome."""
 | 
			
		||||
							
								
								
									
										112
									
								
								esphome/components/cm1106/cm1106.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								esphome/components/cm1106/cm1106.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,112 @@
 | 
			
		||||
#include "cm1106.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cm1106 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "cm1106";
 | 
			
		||||
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
 | 
			
		||||
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
 | 
			
		||||
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
 | 
			
		||||
 | 
			
		||||
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
 | 
			
		||||
  uint8_t crc = 0;
 | 
			
		||||
  for (int i = 0; i < len - 1; i++) {
 | 
			
		||||
    crc -= response[i];
 | 
			
		||||
  }
 | 
			
		||||
  return crc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CM1106Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up CM1106...");
 | 
			
		||||
  uint8_t response[8] = {0};
 | 
			
		||||
  if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with CM1106 failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CM1106Component::update() {
 | 
			
		||||
  uint8_t response[8] = {0};
 | 
			
		||||
  if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
 | 
			
		||||
    ESP_LOGW(TAG, "Reading data from CM1106 failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
 | 
			
		||||
    ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X...", response[0], response[1], response[2],
 | 
			
		||||
             response[3]);
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t checksum = cm1106_checksum(response, sizeof(response));
 | 
			
		||||
  if (response[7] != checksum) {
 | 
			
		||||
    ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
 | 
			
		||||
  uint16_t ppm = response[3] << 8 | response[4];
 | 
			
		||||
  ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
 | 
			
		||||
  if (this->co2_sensor_ != nullptr)
 | 
			
		||||
    this->co2_sensor_->publish_state(ppm);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CM1106Component::calibrate_zero(uint16_t ppm) {
 | 
			
		||||
  uint8_t cmd[6];
 | 
			
		||||
  memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
 | 
			
		||||
  cmd[3] = ppm >> 8;
 | 
			
		||||
  cmd[4] = ppm & 0xFF;
 | 
			
		||||
  uint8_t response[4] = {0};
 | 
			
		||||
 | 
			
		||||
  if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
 | 
			
		||||
    ESP_LOGW(TAG, "Reading data from CM1106 failed!");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // check if correct response received
 | 
			
		||||
  if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
 | 
			
		||||
             response[3]);
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
  ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
 | 
			
		||||
                                            size_t response_len) {
 | 
			
		||||
  // Empty RX Buffer
 | 
			
		||||
  while (this->available())
 | 
			
		||||
    this->read();
 | 
			
		||||
  this->write_array(command, command_len - 1);
 | 
			
		||||
  this->write_byte(cm1106_checksum(command, command_len));
 | 
			
		||||
  this->flush();
 | 
			
		||||
 | 
			
		||||
  if (response == nullptr)
 | 
			
		||||
    return true;
 | 
			
		||||
 | 
			
		||||
  return this->read_array(response, response_len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CM1106Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "CM1106:");
 | 
			
		||||
  LOG_SENSOR("  ", "CO2", this->co2_sensor_);
 | 
			
		||||
  this->check_uart_settings(9600);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with CM1106 failed!");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace cm1106
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										40
									
								
								esphome/components/cm1106/cm1106.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/cm1106/cm1106.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cm1106 {
 | 
			
		||||
 | 
			
		||||
class CM1106Component : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
  void calibrate_zero(uint16_t ppm);
 | 
			
		||||
 | 
			
		||||
  void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *co2_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  CM1106Component *cm1106_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace cm1106
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										72
									
								
								esphome/components/cm1106/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/cm1106/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,72 @@
 | 
			
		||||
"""CM1106 Sensor component for ESPHome."""
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.automation import maybe_simple_id
 | 
			
		||||
from esphome.components import sensor, uart
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CO2,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    ICON_MOLECULE_CO2,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_PARTS_PER_MILLION,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
CODEOWNERS = ["@andrewjswan"]
 | 
			
		||||
 | 
			
		||||
cm1106_ns = cg.esphome_ns.namespace("cm1106")
 | 
			
		||||
CM1106Component = cm1106_ns.class_(
 | 
			
		||||
    "CM1106Component", cg.PollingComponent, uart.UARTDevice
 | 
			
		||||
)
 | 
			
		||||
CM1106CalibrateZeroAction = cm1106_ns.class_(
 | 
			
		||||
    "CM1106CalibrateZeroAction",
 | 
			
		||||
    automation.Action,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CM1106Component),
 | 
			
		||||
            cv.Optional(CONF_CO2): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PARTS_PER_MILLION,
 | 
			
		||||
                icon=ICON_MOLECULE_CO2,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config) -> None:
 | 
			
		||||
    """Code generation entry point."""
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
    if co2_config := config.get(CONF_CO2):
 | 
			
		||||
        sens = await sensor.new_sensor(co2_config)
 | 
			
		||||
        cg.add(var.set_co2_sensor(sens))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(CM1106Component),
 | 
			
		||||
    },
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "cm1106.calibrate_zero",
 | 
			
		||||
    CM1106CalibrateZeroAction,
 | 
			
		||||
    CALIBRATION_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None:
 | 
			
		||||
    """Service code generation entry point."""
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
@@ -2,7 +2,6 @@ import base64
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import secrets
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import requests
 | 
			
		||||
from ruamel.yaml import YAML
 | 
			
		||||
@@ -84,7 +83,7 @@ async def to_code(config):
 | 
			
		||||
def import_config(
 | 
			
		||||
    path: str,
 | 
			
		||||
    name: str,
 | 
			
		||||
    friendly_name: Optional[str],
 | 
			
		||||
    friendly_name: str | None,
 | 
			
		||||
    project_name: str,
 | 
			
		||||
    import_url: str,
 | 
			
		||||
    network: str = CONF_WIFI,
 | 
			
		||||
 
 | 
			
		||||
@@ -34,13 +34,15 @@ class DebugComponent : public PollingComponent {
 | 
			
		||||
#endif
 | 
			
		||||
  void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  void on_shutdown() override;
 | 
			
		||||
  void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
  void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
 | 
			
		||||
    this->cpu_frequency_sensor_ = cpu_frequency_sensor;
 | 
			
		||||
  }
 | 
			
		||||
#endif  // USE_SENSOR
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
  void on_shutdown() override;
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 protected:
 | 
			
		||||
  uint32_t free_heap_{};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,6 @@ import itertools
 | 
			
		||||
import logging
 | 
			
		||||
import os
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
from typing import Optional, Union
 | 
			
		||||
 | 
			
		||||
from esphome import git
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -58,8 +57,10 @@ from .const import (  # noqa
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C2,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32C5,
 | 
			
		||||
    VARIANT_ESP32C6,
 | 
			
		||||
    VARIANT_ESP32H2,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
    VARIANT_FRIENDLY,
 | 
			
		||||
@@ -88,8 +89,10 @@ CPU_FREQUENCIES = {
 | 
			
		||||
    VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
 | 
			
		||||
    VARIANT_ESP32C2: get_cpu_frequencies(80, 120),
 | 
			
		||||
    VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
 | 
			
		||||
    VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240),
 | 
			
		||||
    VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
 | 
			
		||||
    VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
 | 
			
		||||
    VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Make sure not missed here if a new variant added.
 | 
			
		||||
@@ -189,7 +192,7 @@ class RawSdkconfigValue:
 | 
			
		||||
    value: str
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue]
 | 
			
		||||
SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
 | 
			
		||||
@@ -206,8 +209,8 @@ def add_idf_component(
 | 
			
		||||
    ref: str = None,
 | 
			
		||||
    path: str = None,
 | 
			
		||||
    refresh: TimePeriod = None,
 | 
			
		||||
    components: Optional[list[str]] = None,
 | 
			
		||||
    submodules: Optional[list[str]] = None,
 | 
			
		||||
    components: list[str] | None = None,
 | 
			
		||||
    submodules: list[str] | None = None,
 | 
			
		||||
):
 | 
			
		||||
    """Add an esp-idf component to the project."""
 | 
			
		||||
    if not CORE.using_esp_idf:
 | 
			
		||||
@@ -296,11 +299,11 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
 | 
			
		||||
# The default/recommended esp-idf framework version
 | 
			
		||||
#  - https://github.com/espressif/esp-idf/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6)
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2)
 | 
			
		||||
# The platformio/espressif32 version to use for esp-idf frameworks
 | 
			
		||||
#  - https://github.com/platformio/platform-espressif32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION = cv.Version(51, 3, 7)
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13)
 | 
			
		||||
 | 
			
		||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
 | 
			
		||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
 | 
			
		||||
@@ -369,8 +372,8 @@ def _arduino_check_versions(value):
 | 
			
		||||
def _esp_idf_check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 1, 6), None),
 | 
			
		||||
        "dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 3, 2), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,10 @@ from .const import (
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C2,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32C5,
 | 
			
		||||
    VARIANT_ESP32C6,
 | 
			
		||||
    VARIANT_ESP32H2,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
@@ -1592,6 +1594,10 @@ BOARDS = {
 | 
			
		||||
        "name": "Ai-Thinker ESP-C3-M1-I-Kit",
 | 
			
		||||
        "variant": VARIANT_ESP32C3,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-c5-devkitc-1": {
 | 
			
		||||
        "name": "Espressif ESP32-C5-DevKitC-1",
 | 
			
		||||
        "variant": VARIANT_ESP32C5,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-c6-devkitc-1": {
 | 
			
		||||
        "name": "Espressif ESP32-C6-DevKitC-1",
 | 
			
		||||
        "variant": VARIANT_ESP32C6,
 | 
			
		||||
@@ -1632,6 +1638,14 @@ BOARDS = {
 | 
			
		||||
        "name": "Espressif ESP32-H2-DevKit",
 | 
			
		||||
        "variant": VARIANT_ESP32H2,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-p4": {
 | 
			
		||||
        "name": "Espressif ESP32-P4 generic",
 | 
			
		||||
        "variant": VARIANT_ESP32P4,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-p4-evboard": {
 | 
			
		||||
        "name": "Espressif ESP32-P4 Function EV Board",
 | 
			
		||||
        "variant": VARIANT_ESP32P4,
 | 
			
		||||
    },
 | 
			
		||||
    "esp32-pico-devkitm-2": {
 | 
			
		||||
        "name": "Espressif ESP32-PICO-DevKitM-2",
 | 
			
		||||
        "variant": VARIANT_ESP32,
 | 
			
		||||
 
 | 
			
		||||
@@ -17,16 +17,20 @@ VARIANT_ESP32S2 = "ESP32S2"
 | 
			
		||||
VARIANT_ESP32S3 = "ESP32S3"
 | 
			
		||||
VARIANT_ESP32C2 = "ESP32C2"
 | 
			
		||||
VARIANT_ESP32C3 = "ESP32C3"
 | 
			
		||||
VARIANT_ESP32C5 = "ESP32C5"
 | 
			
		||||
VARIANT_ESP32C6 = "ESP32C6"
 | 
			
		||||
VARIANT_ESP32H2 = "ESP32H2"
 | 
			
		||||
VARIANT_ESP32P4 = "ESP32P4"
 | 
			
		||||
VARIANTS = [
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
    VARIANT_ESP32C2,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32C5,
 | 
			
		||||
    VARIANT_ESP32C6,
 | 
			
		||||
    VARIANT_ESP32H2,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
VARIANT_FRIENDLY = {
 | 
			
		||||
@@ -35,8 +39,10 @@ VARIANT_FRIENDLY = {
 | 
			
		||||
    VARIANT_ESP32S3: "ESP32-S3",
 | 
			
		||||
    VARIANT_ESP32C2: "ESP32-C2",
 | 
			
		||||
    VARIANT_ESP32C3: "ESP32-C3",
 | 
			
		||||
    VARIANT_ESP32C5: "ESP32-C5",
 | 
			
		||||
    VARIANT_ESP32C6: "ESP32-C6",
 | 
			
		||||
    VARIANT_ESP32H2: "ESP32-H2",
 | 
			
		||||
    VARIANT_ESP32P4: "ESP32-P4",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp32_ns = cg.esphome_ns.namespace("esp32")
 | 
			
		||||
 
 | 
			
		||||
@@ -15,8 +15,9 @@
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <Esp.h>
 | 
			
		||||
#else
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
 | 
			
		||||
#include <esp_clk_tree.h>
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
void setup();
 | 
			
		||||
void loop();
 | 
			
		||||
#endif
 | 
			
		||||
@@ -63,7 +64,13 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
 | 
			
		||||
uint32_t arch_get_cpu_freq_hz() {
 | 
			
		||||
  uint32_t freq = 0;
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
 | 
			
		||||
  esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
 | 
			
		||||
#else
 | 
			
		||||
  rtc_cpu_freq_config_t config;
 | 
			
		||||
  rtc_clk_cpu_freq_get_config(&config);
 | 
			
		||||
  freq = config.freq_mhz * 1000000U;
 | 
			
		||||
#endif
 | 
			
		||||
#elif defined(USE_ARDUINO)
 | 
			
		||||
  freq = ESP.getCpuFreqMHz() * 1000000;
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any, Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -26,8 +27,10 @@ from .const import (
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C2,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32C5,
 | 
			
		||||
    VARIANT_ESP32C6,
 | 
			
		||||
    VARIANT_ESP32H2,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
    esp32_ns,
 | 
			
		||||
@@ -35,8 +38,10 @@ from .const import (
 | 
			
		||||
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
 | 
			
		||||
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
 | 
			
		||||
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
 | 
			
		||||
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
 | 
			
		||||
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
 | 
			
		||||
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
 | 
			
		||||
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
 | 
			
		||||
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
 | 
			
		||||
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
 | 
			
		||||
 | 
			
		||||
@@ -97,6 +102,10 @@ _esp32_validations = {
 | 
			
		||||
        pin_validation=esp32_c3_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_c3_validate_supports,
 | 
			
		||||
    ),
 | 
			
		||||
    VARIANT_ESP32C5: ESP32ValidationFunctions(
 | 
			
		||||
        pin_validation=esp32_c5_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_c5_validate_supports,
 | 
			
		||||
    ),
 | 
			
		||||
    VARIANT_ESP32C6: ESP32ValidationFunctions(
 | 
			
		||||
        pin_validation=esp32_c6_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_c6_validate_supports,
 | 
			
		||||
@@ -105,6 +114,10 @@ _esp32_validations = {
 | 
			
		||||
        pin_validation=esp32_h2_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_h2_validate_supports,
 | 
			
		||||
    ),
 | 
			
		||||
    VARIANT_ESP32P4: ESP32ValidationFunctions(
 | 
			
		||||
        pin_validation=esp32_p4_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_p4_validate_supports,
 | 
			
		||||
    ),
 | 
			
		||||
    VARIANT_ESP32S2: ESP32ValidationFunctions(
 | 
			
		||||
        pin_validation=esp32_s2_validate_gpio_pin,
 | 
			
		||||
        usage_validation=esp32_s2_validate_supports,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										45
									
								
								esphome/components/esp32/gpio_esp32_c5.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/esp32/gpio_esp32_c5.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,45 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
 | 
			
		||||
from esphome.pins import check_strapping_pin
 | 
			
		||||
 | 
			
		||||
_ESP32C5_SPI_PSRAM_PINS = {
 | 
			
		||||
    16: "SPICS0",
 | 
			
		||||
    17: "SPIQ",
 | 
			
		||||
    18: "SPIWP",
 | 
			
		||||
    19: "VDD_SPI",
 | 
			
		||||
    20: "SPIHD",
 | 
			
		||||
    21: "SPICLK",
 | 
			
		||||
    22: "SPID",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def esp32_c5_validate_gpio_pin(value):
 | 
			
		||||
    if value < 0 or value > 28:
 | 
			
		||||
        raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
 | 
			
		||||
    if value in _ESP32C5_SPI_PSRAM_PINS:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def esp32_c5_validate_supports(value):
 | 
			
		||||
    num = value[CONF_NUMBER]
 | 
			
		||||
    mode = value[CONF_MODE]
 | 
			
		||||
    is_input = mode[CONF_INPUT]
 | 
			
		||||
 | 
			
		||||
    if num < 0 or num > 28:
 | 
			
		||||
        raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
 | 
			
		||||
    if is_input:
 | 
			
		||||
        # All ESP32 pins support input mode
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
 | 
			
		||||
    return value
 | 
			
		||||
							
								
								
									
										43
									
								
								esphome/components/esp32/gpio_esp32_p4.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/esp32/gpio_esp32_p4.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
 | 
			
		||||
 | 
			
		||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
 | 
			
		||||
 | 
			
		||||
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def esp32_p4_validate_gpio_pin(value):
 | 
			
		||||
    if value < 0 or value > 54:
 | 
			
		||||
        raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
 | 
			
		||||
    if value in _ESP32P4_STRAPPING_PINS:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "GPIO%d is a Strapping PIN and should be avoided.\n"
 | 
			
		||||
            "Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
 | 
			
		||||
            "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
 | 
			
		||||
            value,
 | 
			
		||||
        )
 | 
			
		||||
    if value in _ESP32P4_USB_JTAG_PINS:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
 | 
			
		||||
            "To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
 | 
			
		||||
            value,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def esp32_p4_validate_supports(value):
 | 
			
		||||
    num = value[CONF_NUMBER]
 | 
			
		||||
    mode = value[CONF_MODE]
 | 
			
		||||
    is_input = mode[CONF_INPUT]
 | 
			
		||||
 | 
			
		||||
    if num < 0 or num > 54:
 | 
			
		||||
        raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
 | 
			
		||||
    if is_input:
 | 
			
		||||
        # All ESP32 pins support input mode
 | 
			
		||||
        pass
 | 
			
		||||
    return value
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from collections.abc import MutableMapping
 | 
			
		||||
from collections.abc import Callable, MutableMapping
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any, Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,8 @@ void ESPHomeOTAComponent::handle_() {
 | 
			
		||||
  int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
 | 
			
		||||
    client_->close();
 | 
			
		||||
    client_ = nullptr;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ from esphome.components.esp32 import get_esp32_variant
 | 
			
		||||
from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
@@ -74,6 +75,7 @@ I2S_PORTS = {
 | 
			
		||||
    VARIANT_ESP32S2: 1,
 | 
			
		||||
    VARIANT_ESP32S3: 2,
 | 
			
		||||
    VARIANT_ESP32C3: 1,
 | 
			
		||||
    VARIANT_ESP32P4: 3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from typing import Callable
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,8 +8,10 @@ from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C2,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
    VARIANT_ESP32C5,
 | 
			
		||||
    VARIANT_ESP32C6,
 | 
			
		||||
    VARIANT_ESP32H2,
 | 
			
		||||
    VARIANT_ESP32P4,
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
@@ -24,6 +26,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_HARDWARE_UART,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LEVEL,
 | 
			
		||||
    CONF_LOGGER,
 | 
			
		||||
    CONF_LOGS,
 | 
			
		||||
    CONF_ON_MESSAGE,
 | 
			
		||||
    CONF_TAG,
 | 
			
		||||
@@ -87,8 +90,10 @@ UART_SELECTION_ESP32 = {
 | 
			
		||||
    VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
    VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
    VARIANT_ESP32C2: [UART0, UART1],
 | 
			
		||||
    VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
    VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
    VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
    VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
 | 
			
		||||
@@ -204,8 +209,10 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                esp32_s3_idf=USB_SERIAL_JTAG,
 | 
			
		||||
                esp32_c3_arduino=USB_CDC,
 | 
			
		||||
                esp32_c3_idf=USB_SERIAL_JTAG,
 | 
			
		||||
                esp32_c5_idf=USB_SERIAL_JTAG,
 | 
			
		||||
                esp32_c6_arduino=USB_CDC,
 | 
			
		||||
                esp32_c6_idf=USB_SERIAL_JTAG,
 | 
			
		||||
                esp32_p4_idf=USB_SERIAL_JTAG,
 | 
			
		||||
                rp2040=USB_CDC,
 | 
			
		||||
                bk72xx=DEFAULT,
 | 
			
		||||
                rtl87xx=DEFAULT,
 | 
			
		||||
@@ -247,6 +254,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    baud_rate = config[CONF_BAUD_RATE]
 | 
			
		||||
    level = config[CONF_LEVEL]
 | 
			
		||||
    CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
 | 
			
		||||
    initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
 | 
			
		||||
    log = cg.new_Pvariable(
 | 
			
		||||
        config[CONF_ID],
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,12 @@
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "freertos/FreeRTOS.h"
 | 
			
		||||
#include "esp_idf_version.h"
 | 
			
		||||
#include "freertos/FreeRTOS.h"
 | 
			
		||||
 | 
			
		||||
#include <fcntl.h>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <fcntl.h>
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
@@ -174,11 +174,11 @@ void Logger::pre_setup() {
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
void HOT Logger::write_msg_(const char *msg) {
 | 
			
		||||
  if (
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2)
 | 
			
		||||
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
 | 
			
		||||
      this->uart_ == UART_SELECTION_USB_CDC
 | 
			
		||||
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
 | 
			
		||||
      this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
 | 
			
		||||
#elif defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
 | 
			
		||||
      this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
 | 
			
		||||
#else
 | 
			
		||||
      /* DISABLES CODE */ (false)  // NOLINT
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.cpp_helpers import register_component, register_parented
 | 
			
		||||
 | 
			
		||||
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns
 | 
			
		||||
from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
 | 
			
		||||
@@ -21,9 +21,10 @@ CONFIG_SCHEMA = select.select_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    levels = LOG_LEVEL_SEVERITY
 | 
			
		||||
    index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL])
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_LOGGER_ID])
 | 
			
		||||
    levels = list(LOG_LEVELS)
 | 
			
		||||
    index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL])
 | 
			
		||||
    levels = levels[: index + 1]
 | 
			
		||||
    var = await select.new_select(config, options=levels)
 | 
			
		||||
    await register_parented(var, config[CONF_LOGGER_ID])
 | 
			
		||||
    await register_parented(var, parent)
 | 
			
		||||
    await register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -321,7 +321,7 @@ async def to_code(configs):
 | 
			
		||||
            frac = 2
 | 
			
		||||
        elif frac > 0.19:
 | 
			
		||||
            frac = 4
 | 
			
		||||
        else:
 | 
			
		||||
        elif frac != 0:
 | 
			
		||||
            frac = 8
 | 
			
		||||
        displays = [
 | 
			
		||||
            await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]
 | 
			
		||||
@@ -422,7 +422,7 @@ LVGL_SCHEMA = cv.All(
 | 
			
		||||
                ): lvalid.lv_font,
 | 
			
		||||
                cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
 | 
			
		||||
                cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
 | 
			
		||||
                cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
 | 
			
		||||
                cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
 | 
			
		||||
                cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
 | 
			
		||||
                    *df.LV_LOG_LEVELS, upper=True
 | 
			
		||||
                ),
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
from typing import Any, Callable
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import image
 | 
			
		||||
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
 | 
			
		||||
@@ -361,7 +359,7 @@ lv_image_list = LValidator(
 | 
			
		||||
lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def lv_pct(value: Union[int, float]):
 | 
			
		||||
def lv_pct(value: int | float):
 | 
			
		||||
    if isinstance(value, float):
 | 
			
		||||
        value = int(value * 100)
 | 
			
		||||
    return literal(f"lv_pct({value})")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import abc
 | 
			
		||||
from typing import Union
 | 
			
		||||
 | 
			
		||||
from esphome import codegen as cg
 | 
			
		||||
from esphome.config import Config
 | 
			
		||||
@@ -75,7 +74,7 @@ class CodeContext(abc.ABC):
 | 
			
		||||
    code_context = None
 | 
			
		||||
 | 
			
		||||
    @abc.abstractmethod
 | 
			
		||||
    def add(self, expression: Union[Expression, Statement]):
 | 
			
		||||
    def add(self, expression: Expression | Statement):
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
@@ -89,13 +88,13 @@ class CodeContext(abc.ABC):
 | 
			
		||||
        CodeContext.append(RawStatement("}"))
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def append(expression: Union[Expression, Statement]):
 | 
			
		||||
    def append(expression: Expression | Statement):
 | 
			
		||||
        if CodeContext.code_context is not None:
 | 
			
		||||
            CodeContext.code_context.add(expression)
 | 
			
		||||
        return expression
 | 
			
		||||
 | 
			
		||||
    def __init__(self):
 | 
			
		||||
        self.previous: Union[CodeContext | None] = None
 | 
			
		||||
        self.previous: CodeContext | None = None
 | 
			
		||||
        self.indent_level = 0
 | 
			
		||||
 | 
			
		||||
    async def __aenter__(self):
 | 
			
		||||
@@ -121,7 +120,7 @@ class MainContext(CodeContext):
 | 
			
		||||
    Code generation into the main() function
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def add(self, expression: Union[Expression, Statement]):
 | 
			
		||||
    def add(self, expression: Expression | Statement):
 | 
			
		||||
        return cg.add(self.indented_statement(expression))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -144,7 +143,7 @@ class LambdaContext(CodeContext):
 | 
			
		||||
        self.capture = capture
 | 
			
		||||
        self.where = where
 | 
			
		||||
 | 
			
		||||
    def add(self, expression: Union[Expression, Statement]):
 | 
			
		||||
    def add(self, expression: Expression | Statement):
 | 
			
		||||
        self.code_list.append(self.indented_statement(expression))
 | 
			
		||||
        return expression
 | 
			
		||||
 | 
			
		||||
@@ -186,7 +185,7 @@ class LvContext(LambdaContext):
 | 
			
		||||
    async def __aexit__(self, exc_type, exc_val, exc_tb):
 | 
			
		||||
        await super().__aexit__(exc_type, exc_val, exc_tb)
 | 
			
		||||
 | 
			
		||||
    def add(self, expression: Union[Expression, Statement]):
 | 
			
		||||
    def add(self, expression: Expression | Statement):
 | 
			
		||||
        cg.add(expression)
 | 
			
		||||
        return expression
 | 
			
		||||
 | 
			
		||||
@@ -303,7 +302,7 @@ lvgl_static = MockObj("LvglComponent", "::")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# equivalent to cg.add() for the current code context
 | 
			
		||||
def lv_add(expression: Union[Expression, Statement]):
 | 
			
		||||
def lv_add(expression: Expression | Statement):
 | 
			
		||||
    return CodeContext.append(expression)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,8 @@ namespace esphome {
 | 
			
		||||
namespace lvgl {
 | 
			
		||||
static const char *const TAG = "lvgl";
 | 
			
		||||
 | 
			
		||||
static const size_t MIN_BUFFER_FRAC = 8;
 | 
			
		||||
 | 
			
		||||
static const char *const EVENT_NAMES[] = {
 | 
			
		||||
    "NONE",
 | 
			
		||||
    "PRESSED",
 | 
			
		||||
@@ -85,6 +87,7 @@ lv_event_code_t lv_update_event;  // NOLINT
 | 
			
		||||
void LvglComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "LVGL:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Buffer size: %zu%%", 100 / this->buffer_frac_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Rotation: %d", this->rotation);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Draw rounding: %d", (int) this->draw_rounding);
 | 
			
		||||
}
 | 
			
		||||
@@ -432,18 +435,28 @@ void LvglComponent::setup() {
 | 
			
		||||
  auto *display = this->displays_[0];
 | 
			
		||||
  auto width = display->get_width();
 | 
			
		||||
  auto height = display->get_height();
 | 
			
		||||
  size_t buffer_pixels = width * height / this->buffer_frac_;
 | 
			
		||||
  auto frac = this->buffer_frac_;
 | 
			
		||||
  if (frac == 0)
 | 
			
		||||
    frac = 1;
 | 
			
		||||
  size_t buffer_pixels = width * height / frac;
 | 
			
		||||
  auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
 | 
			
		||||
  void *buffer = nullptr;
 | 
			
		||||
  if (this->buffer_frac_ >= 4)
 | 
			
		||||
  if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
 | 
			
		||||
    buffer = malloc(buf_bytes);  // NOLINT
 | 
			
		||||
  if (buffer == nullptr)
 | 
			
		||||
    buffer = lv_custom_mem_alloc(buf_bytes);  // NOLINT
 | 
			
		||||
  // if specific buffer size not set and can't get 100%, try for a smaller one
 | 
			
		||||
  if (buffer == nullptr && this->buffer_frac_ == 0) {
 | 
			
		||||
    frac = MIN_BUFFER_FRAC;
 | 
			
		||||
    buffer_pixels /= MIN_BUFFER_FRAC;
 | 
			
		||||
    buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC);  // NOLINT
 | 
			
		||||
  }
 | 
			
		||||
  if (buffer == nullptr) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    this->status_set_error("Memory allocation failure");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->buffer_frac_ = frac;
 | 
			
		||||
  lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
 | 
			
		||||
  this->disp_drv_.hor_res = width;
 | 
			
		||||
  this->disp_drv_.ver_res = height;
 | 
			
		||||
@@ -453,8 +466,8 @@ void LvglComponent::setup() {
 | 
			
		||||
  if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
 | 
			
		||||
    this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes));  // NOLINT
 | 
			
		||||
    if (this->rotate_buf_ == nullptr) {
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      this->status_set_error("Memory allocation failure");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -36,29 +36,43 @@ from .types import (
 | 
			
		||||
# this will be populated later, in __init__.py to avoid circular imports.
 | 
			
		||||
WIDGET_TYPES: dict = {}
 | 
			
		||||
 | 
			
		||||
TIME_TEXT_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_TIME_FORMAT): cv.string,
 | 
			
		||||
        cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
PRINTF_TEXT_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_FORMAT): cv.string,
 | 
			
		||||
            cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
 | 
			
		||||
        },
 | 
			
		||||
    ),
 | 
			
		||||
    validate_printf,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_text(value):
 | 
			
		||||
    """
 | 
			
		||||
    Do some sanity checking of the format to get better error messages
 | 
			
		||||
    than using cv.Any
 | 
			
		||||
    """
 | 
			
		||||
    if value is None:
 | 
			
		||||
        raise cv.Invalid("No text specified")
 | 
			
		||||
    if isinstance(value, dict):
 | 
			
		||||
        if CONF_TIME_FORMAT in value:
 | 
			
		||||
            return TIME_TEXT_SCHEMA(value)
 | 
			
		||||
        return PRINTF_TEXT_SCHEMA(value)
 | 
			
		||||
 | 
			
		||||
    return cv.templatable(cv.string)(value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# A schema for text properties
 | 
			
		||||
TEXT_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_TEXT): cv.Any(
 | 
			
		||||
            cv.All(
 | 
			
		||||
                cv.Schema(
 | 
			
		||||
                    {
 | 
			
		||||
                        cv.Required(CONF_FORMAT): cv.string,
 | 
			
		||||
                        cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
 | 
			
		||||
                            cv.lambda_
 | 
			
		||||
                        ),
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                validate_printf,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_TIME_FORMAT): cv.string,
 | 
			
		||||
                    cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.templatable(cv.string),
 | 
			
		||||
        )
 | 
			
		||||
        cv.Optional(CONF_TEXT): _validate_text,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -247,11 +261,13 @@ FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
 | 
			
		||||
def part_schema(parts):
 | 
			
		||||
    """
 | 
			
		||||
    Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
 | 
			
		||||
    :param parts:  The parts to include in the schema
 | 
			
		||||
    :param parts:  The parts to include
 | 
			
		||||
    :return: The schema
 | 
			
		||||
    """
 | 
			
		||||
    return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
 | 
			
		||||
        STATE_SCHEMA
 | 
			
		||||
    return (
 | 
			
		||||
        cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts})
 | 
			
		||||
        .extend(STATE_SCHEMA)
 | 
			
		||||
        .extend(FLAG_SCHEMA)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -288,22 +304,18 @@ def base_update_schema(widget_type, parts):
 | 
			
		||||
    :param parts:  The allowable parts to specify
 | 
			
		||||
    :return:
 | 
			
		||||
    """
 | 
			
		||||
    return (
 | 
			
		||||
        part_schema(parts)
 | 
			
		||||
        .extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_ID): cv.ensure_list(
 | 
			
		||||
                    cv.maybe_simple_value(
 | 
			
		||||
                        {
 | 
			
		||||
                            cv.Required(CONF_ID): cv.use_id(widget_type),
 | 
			
		||||
                        },
 | 
			
		||||
                        key=CONF_ID,
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        .extend(FLAG_SCHEMA)
 | 
			
		||||
    return part_schema(parts).extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.ensure_list(
 | 
			
		||||
                cv.maybe_simple_value(
 | 
			
		||||
                    {
 | 
			
		||||
                        cv.Required(CONF_ID): cv.use_id(widget_type),
 | 
			
		||||
                    },
 | 
			
		||||
                    key=CONF_ID,
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -321,7 +333,6 @@ def obj_schema(widget_type: WidgetType):
 | 
			
		||||
    """
 | 
			
		||||
    return (
 | 
			
		||||
        part_schema(widget_type.parts)
 | 
			
		||||
        .extend(FLAG_SCHEMA)
 | 
			
		||||
        .extend(LAYOUT_SCHEMA)
 | 
			
		||||
        .extend(ALIGN_TO_SCHEMA)
 | 
			
		||||
        .extend(automation_schema(widget_type.w_type))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import sys
 | 
			
		||||
from typing import Any, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import codegen as cg, config_validation as cv
 | 
			
		||||
from esphome.config_validation import Invalid
 | 
			
		||||
@@ -262,7 +262,7 @@ async def wait_for_widgets():
 | 
			
		||||
    await FakeAwaitable(widgets_wait_generator())
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]:
 | 
			
		||||
async def get_widgets(config: dict | list, id: str = CONF_ID) -> list[Widget]:
 | 
			
		||||
    if not config:
 | 
			
		||||
        return []
 | 
			
		||||
    if not isinstance(config, list):
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ from .obj import obj_spec
 | 
			
		||||
 | 
			
		||||
CONF_TABVIEW = "tabview"
 | 
			
		||||
CONF_TAB_STYLE = "tab_style"
 | 
			
		||||
CONF_CONTENT_STYLE = "content_style"
 | 
			
		||||
 | 
			
		||||
lv_tab_t = LvType("lv_obj_t")
 | 
			
		||||
 | 
			
		||||
@@ -39,6 +40,7 @@ TABVIEW_SCHEMA = cv.Schema(
 | 
			
		||||
            )
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
 | 
			
		||||
        cv.Optional(CONF_CONTENT_STYLE): part_schema(obj_spec.parts),
 | 
			
		||||
        cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
 | 
			
		||||
        cv.Optional(CONF_SIZE, default="10%"): size,
 | 
			
		||||
    }
 | 
			
		||||
@@ -79,6 +81,11 @@ class TabviewType(WidgetType):
 | 
			
		||||
                "tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
 | 
			
		||||
            ) as btnmatrix_obj:
 | 
			
		||||
                await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
 | 
			
		||||
        if content_style := config.get(CONF_CONTENT_STYLE):
 | 
			
		||||
            with LocalVariable(
 | 
			
		||||
                "tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
 | 
			
		||||
            ) as content_obj:
 | 
			
		||||
                await set_obj_properties(Widget(content_obj, obj_spec), content_style)
 | 
			
		||||
 | 
			
		||||
    def obj_creator(self, parent: MockObjClass, config: dict):
 | 
			
		||||
        return lv_expr.call(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										52
									
								
								esphome/components/max7219digit/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/max7219digit/automation.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,52 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include "max7219digit.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace max7219digit {
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayInvertAction : public Action<Ts...>, public Parented<MAX7219Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, state)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    bool state = this->state_.value(x...);
 | 
			
		||||
    this->parent_->invert_on_off(state);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayVisibilityAction : public Action<Ts...>, public Parented<MAX7219Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, state)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    bool state = this->state_.value(x...);
 | 
			
		||||
    this->parent_->turn_on_off(state);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayReverseAction : public Action<Ts...>, public Parented<MAX7219Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(bool, state)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    bool state = this->state_.value(x...);
 | 
			
		||||
    this->parent_->set_reverse(state);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class DisplayIntensityAction : public Action<Ts...>, public Parented<MAX7219Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  TEMPLATABLE_VALUE(uint8_t, state)
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    uint8_t state = this->state_.value(x...);
 | 
			
		||||
    this->parent_->set_intensity(state);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace max7219digit
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,7 +1,14 @@
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import display, spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INTENSITY,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_NUM_CHIPS,
 | 
			
		||||
    CONF_STATE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@rspaargaren"]
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
@@ -17,6 +24,7 @@ CONF_REVERSE_ENABLE = "reverse_enable"
 | 
			
		||||
CONF_NUM_CHIP_LINES = "num_chip_lines"
 | 
			
		||||
CONF_CHIP_LINES_STYLE = "chip_lines_style"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
integration_ns = cg.esphome_ns.namespace("max7219digit")
 | 
			
		||||
ChipLinesStyle = integration_ns.enum("ChipLinesStyle")
 | 
			
		||||
CHIP_LINES_STYLE = {
 | 
			
		||||
@@ -99,3 +107,87 @@ async def to_code(config):
 | 
			
		||||
            config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DisplayInvertAction = max7219_ns.class_("DisplayInvertAction", automation.Action)
 | 
			
		||||
DisplayVisibilityAction = max7219_ns.class_(
 | 
			
		||||
    "DisplayVisibilityAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
DisplayReverseAction = max7219_ns.class_("DisplayReverseAction", automation.Action)
 | 
			
		||||
DisplayIntensityAction = max7219_ns.class_("DisplayIntensityAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MAX7219_OFF_ACTION_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(MAX7219Component),
 | 
			
		||||
        cv.Optional(CONF_STATE, default=False): False,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(MAX7219Component),
 | 
			
		||||
        cv.Optional(CONF_STATE, default=True): True,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def max7129digit_invert_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_state(config[CONF_STATE]))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def max7129digit_visible_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_state(config[CONF_STATE]))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def max7129digit_reverse_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    cg.add(var.set_state(config[CONF_STATE]))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(MAX7219Component),
 | 
			
		||||
        cv.Optional(CONF_INTENSITY, default=15): cv.templatable(
 | 
			
		||||
            cv.int_range(min=0, max=15)
 | 
			
		||||
        ),
 | 
			
		||||
    },
 | 
			
		||||
    key=CONF_INTENSITY,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def max7129digit_intensity_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg)
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ID])
 | 
			
		||||
    template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8)
 | 
			
		||||
    cg.add(var.set_state(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 
 | 
			
		||||
@@ -147,7 +147,11 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES
 | 
			
		||||
      this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0];  // probability;
 | 
			
		||||
      this->unprocessed_probability_status_ = true;
 | 
			
		||||
    }
 | 
			
		||||
    this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
 | 
			
		||||
    if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) {
 | 
			
		||||
      // Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a
 | 
			
		||||
      // previous detection and calling ``reset_probabilities`` so it avoids duplicate detections
 | 
			
		||||
      this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import switch
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ADDRESS, CONF_ID
 | 
			
		||||
from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID
 | 
			
		||||
 | 
			
		||||
from .. import (
 | 
			
		||||
    MODBUS_REGISTER_TYPE,
 | 
			
		||||
@@ -36,6 +36,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    .extend(ModbusItemBaseSchema)
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
 | 
			
		||||
            cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
 | 
			
		||||
@@ -62,7 +63,10 @@ async def to_code(config):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
 | 
			
		||||
    cg.add(var.set_parent(paren))
 | 
			
		||||
    cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
 | 
			
		||||
    cg.add(paren.add_sensor_item(var))
 | 
			
		||||
    assumed_state = config[CONF_ASSUMED_STATE]
 | 
			
		||||
    cg.add(var.set_assumed_state(assumed_state))
 | 
			
		||||
    if not assumed_state:
 | 
			
		||||
        cg.add(paren.add_sensor_item(var))
 | 
			
		||||
    if CONF_WRITE_LAMBDA in config:
 | 
			
		||||
        template_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_WRITE_LAMBDA],
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,10 @@ void ModbusSwitch::setup() {
 | 
			
		||||
}
 | 
			
		||||
void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); }
 | 
			
		||||
 | 
			
		||||
void ModbusSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
 | 
			
		||||
 | 
			
		||||
bool ModbusSwitch::assumed_state() { return this->assumed_state_; }
 | 
			
		||||
 | 
			
		||||
void ModbusSwitch::parse_and_publish(const std::vector<uint8_t> &data) {
 | 
			
		||||
  bool value = false;
 | 
			
		||||
  switch (this->register_type) {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,6 +29,7 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void write_state(bool state) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_assumed_state(bool assumed_state);
 | 
			
		||||
  void set_state(bool state) { this->state = state; }
 | 
			
		||||
  void parse_and_publish(const std::vector<uint8_t> &data) override;
 | 
			
		||||
  void set_parent(ModbusController *parent) { this->parent_ = parent; }
 | 
			
		||||
@@ -40,10 +41,12 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
 | 
			
		||||
  void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool assumed_state() override;
 | 
			
		||||
  ModbusController *parent_{nullptr};
 | 
			
		||||
  bool use_write_multiple_{false};
 | 
			
		||||
  optional<transform_func_t> publish_transform_func_{nullopt};
 | 
			
		||||
  optional<write_transform_func_t> write_transform_func_{nullopt};
 | 
			
		||||
  bool assumed_state_{false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace modbus_controller
 | 
			
		||||
 
 | 
			
		||||
@@ -21,8 +21,10 @@ from esphome.const import (
 | 
			
		||||
    CONF_WEB_SERVER,
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_AQI,
 | 
			
		||||
    DEVICE_CLASS_AREA,
 | 
			
		||||
    DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CONDUCTIVITY,
 | 
			
		||||
@@ -33,6 +35,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_DURATION,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_DISTANCE,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_GAS,
 | 
			
		||||
@@ -54,6 +57,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION,
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION_INTENSITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    DEVICE_CLASS_SOUND_PRESSURE,
 | 
			
		||||
@@ -68,6 +72,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_VOLUME_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_WATER,
 | 
			
		||||
    DEVICE_CLASS_WEIGHT,
 | 
			
		||||
    DEVICE_CLASS_WIND_DIRECTION,
 | 
			
		||||
    DEVICE_CLASS_WIND_SPEED,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
@@ -78,8 +83,10 @@ CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_AQI,
 | 
			
		||||
    DEVICE_CLASS_AREA,
 | 
			
		||||
    DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CONDUCTIVITY,
 | 
			
		||||
@@ -90,6 +97,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_DURATION,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_DISTANCE,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_GAS,
 | 
			
		||||
@@ -111,6 +119,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION,
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION_INTENSITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    DEVICE_CLASS_SOUND_PRESSURE,
 | 
			
		||||
@@ -125,6 +134,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_VOLUME_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_WATER,
 | 
			
		||||
    DEVICE_CLASS_WEIGHT,
 | 
			
		||||
    DEVICE_CLASS_WIND_DIRECTION,
 | 
			
		||||
    DEVICE_CLASS_WIND_SPEED,
 | 
			
		||||
]
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ class PNGFormat(Format):
 | 
			
		||||
 | 
			
		||||
    def actions(self):
 | 
			
		||||
        cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
 | 
			
		||||
        cg.add_library("pngle", "1.0.2")
 | 
			
		||||
        cg.add_library("pngle", "1.1.0")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
IMAGE_FORMATS = {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,12 +34,32 @@ static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) {
 | 
			
		||||
 * @param h The height of the rectangle to draw.
 | 
			
		||||
 * @param rgba The color to paint the rectangle in.
 | 
			
		||||
 */
 | 
			
		||||
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
 | 
			
		||||
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
 | 
			
		||||
  PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
 | 
			
		||||
  Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
 | 
			
		||||
  decoder->draw(x, y, w, h, color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
 | 
			
		||||
  {
 | 
			
		||||
    pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
 | 
			
		||||
    if (!pngle) {
 | 
			
		||||
      ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    memset(pngle, 0, PNGLE_T_SIZE);
 | 
			
		||||
    pngle_reset(pngle);
 | 
			
		||||
    this->pngle_ = pngle;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
PngDecoder::~PngDecoder() {
 | 
			
		||||
  if (this->pngle_) {
 | 
			
		||||
    pngle_reset(this->pngle_);
 | 
			
		||||
    this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int PngDecoder::prepare(size_t download_size) {
 | 
			
		||||
  ImageDecoder::prepare(download_size);
 | 
			
		||||
  if (!this->pngle_) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "image_decoder.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "image_decoder.h"
 | 
			
		||||
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
 | 
			
		||||
#include <pngle.h>
 | 
			
		||||
 | 
			
		||||
@@ -18,13 +19,14 @@ class PngDecoder : public ImageDecoder {
 | 
			
		||||
   *
 | 
			
		||||
   * @param display The image to decode the stream into.
 | 
			
		||||
   */
 | 
			
		||||
  PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
 | 
			
		||||
  ~PngDecoder() override { pngle_destroy(this->pngle_); }
 | 
			
		||||
  PngDecoder(OnlineImage *image);
 | 
			
		||||
  ~PngDecoder() override;
 | 
			
		||||
 | 
			
		||||
  int prepare(size_t download_size) override;
 | 
			
		||||
  int HOT decode(uint8_t *buffer, size_t size) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  RAMAllocator<pngle_t> allocator_;
 | 
			
		||||
  pngle_t *pngle_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
from collections.abc import Awaitable
 | 
			
		||||
from typing import Any, Callable, Optional
 | 
			
		||||
from collections.abc import Awaitable, Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
@@ -103,7 +103,7 @@ def define_setting_readers(component_type: str, keys: list[str]) -> None:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
 | 
			
		||||
    messages: dict[str, tuple[bool, Optional[int]]] = {}
 | 
			
		||||
    messages: dict[str, tuple[bool, int | None]] = {}
 | 
			
		||||
    for key in keys:
 | 
			
		||||
        messages[schemas[key].message] = (
 | 
			
		||||
            schemas[key].keep_updated,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
# inputs of the OpenTherm component.
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from typing import Any, Optional, TypeVar
 | 
			
		||||
from typing import Any, TypeVar
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
@@ -61,11 +61,11 @@ TSchema = TypeVar("TSchema", bound=EntitySchema)
 | 
			
		||||
class SensorSchema(EntitySchema):
 | 
			
		||||
    accuracy_decimals: int
 | 
			
		||||
    state_class: str
 | 
			
		||||
    unit_of_measurement: Optional[str] = None
 | 
			
		||||
    icon: Optional[str] = None
 | 
			
		||||
    device_class: Optional[str] = None
 | 
			
		||||
    unit_of_measurement: str | None = None
 | 
			
		||||
    icon: str | None = None
 | 
			
		||||
    device_class: str | None = None
 | 
			
		||||
    disabled_by_default: bool = False
 | 
			
		||||
    order: Optional[int] = None
 | 
			
		||||
    order: int | None = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SENSORS: dict[str, SensorSchema] = {
 | 
			
		||||
@@ -461,9 +461,9 @@ SENSORS: dict[str, SensorSchema] = {
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class BinarySensorSchema(EntitySchema):
 | 
			
		||||
    icon: Optional[str] = None
 | 
			
		||||
    device_class: Optional[str] = None
 | 
			
		||||
    order: Optional[int] = None
 | 
			
		||||
    icon: str | None = None
 | 
			
		||||
    device_class: str | None = None
 | 
			
		||||
    order: int | None = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
BINARY_SENSORS: dict[str, BinarySensorSchema] = {
 | 
			
		||||
@@ -654,7 +654,7 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = {
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class SwitchSchema(EntitySchema):
 | 
			
		||||
    default_mode: Optional[str] = None
 | 
			
		||||
    default_mode: str | None = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SWITCHES: dict[str, SwitchSchema] = {
 | 
			
		||||
@@ -721,9 +721,9 @@ class InputSchema(EntitySchema):
 | 
			
		||||
    unit_of_measurement: str
 | 
			
		||||
    step: float
 | 
			
		||||
    range: tuple[int, int]
 | 
			
		||||
    icon: Optional[str] = None
 | 
			
		||||
    auto_max_value: Optional[AutoConfigure] = None
 | 
			
		||||
    auto_min_value: Optional[AutoConfigure] = None
 | 
			
		||||
    icon: str | None = None
 | 
			
		||||
    auto_max_value: AutoConfigure | None = None
 | 
			
		||||
    auto_min_value: AutoConfigure | None = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
INPUTS: dict[str, InputSchema] = {
 | 
			
		||||
@@ -834,7 +834,7 @@ class SettingSchema(EntitySchema):
 | 
			
		||||
    backing_type: str
 | 
			
		||||
    validation_schema: cv.Schema
 | 
			
		||||
    default_value: Any
 | 
			
		||||
    order: Optional[int] = None
 | 
			
		||||
    order: int | None = None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SETTINGS: dict[str, SettingSchema] = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
from typing import Callable
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
 | 
			
		||||
from voluptuous import Schema
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.esp32 import (
 | 
			
		||||
    CONF_CPU_FREQUENCY,
 | 
			
		||||
    CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    add_idf_sdkconfig_option,
 | 
			
		||||
@@ -50,18 +51,23 @@ SPIRAM_SPEEDS = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_psram_mode(config):
 | 
			
		||||
    if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
 | 
			
		||||
        esp32_config = fv.full_config.get()[PLATFORM_ESP32]
 | 
			
		||||
        if (
 | 
			
		||||
            esp32_config[CONF_FRAMEWORK]
 | 
			
		||||
            .get(CONF_ADVANCED, {})
 | 
			
		||||
            .get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
 | 
			
		||||
        ):
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
 | 
			
		||||
    esp32_config = fv.full_config.get()[PLATFORM_ESP32]
 | 
			
		||||
    if config[CONF_SPEED] == 120e6:
 | 
			
		||||
        if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "PSRAM 120MHz requires 240MHz CPU frequency (set in esp32 component)"
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
 | 
			
		||||
        if config[CONF_MODE] == TYPE_OCTAL:
 | 
			
		||||
            if (
 | 
			
		||||
                esp32_config[CONF_FRAMEWORK]
 | 
			
		||||
                .get(CONF_ADVANCED, {})
 | 
			
		||||
                .get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
 | 
			
		||||
            ):
 | 
			
		||||
                _LOGGER.warning(
 | 
			
		||||
                    "120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
 | 
			
		||||
    if config[CONF_MODE] != TYPE_OCTAL and config[CONF_ENABLE_ECC]:
 | 
			
		||||
        raise cv.Invalid("ECC is only available in octal mode.")
 | 
			
		||||
    if config[CONF_MODE] == TYPE_OCTAL:
 | 
			
		||||
@@ -112,7 +118,7 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True)
 | 
			
		||||
        add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True)
 | 
			
		||||
        if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240", True)
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
 | 
			
		||||
            if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
 | 
			
		||||
                add_idf_sdkconfig_option(
 | 
			
		||||
                    "CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True
 | 
			
		||||
 
 | 
			
		||||
@@ -43,8 +43,10 @@ from esphome.const import (
 | 
			
		||||
    CONF_WINDOW_SIZE,
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_AQI,
 | 
			
		||||
    DEVICE_CLASS_AREA,
 | 
			
		||||
    DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CONDUCTIVITY,
 | 
			
		||||
@@ -56,6 +58,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_DURATION,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_DISTANCE,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_GAS,
 | 
			
		||||
@@ -77,6 +80,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION,
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION_INTENSITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    DEVICE_CLASS_SOUND_PRESSURE,
 | 
			
		||||
@@ -92,6 +96,7 @@ from esphome.const import (
 | 
			
		||||
    DEVICE_CLASS_VOLUME_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_WATER,
 | 
			
		||||
    DEVICE_CLASS_WEIGHT,
 | 
			
		||||
    DEVICE_CLASS_WIND_DIRECTION,
 | 
			
		||||
    DEVICE_CLASS_WIND_SPEED,
 | 
			
		||||
    ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
)
 | 
			
		||||
@@ -104,8 +109,10 @@ CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_APPARENT_POWER,
 | 
			
		||||
    DEVICE_CLASS_AQI,
 | 
			
		||||
    DEVICE_CLASS_AREA,
 | 
			
		||||
    DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_BATTERY,
 | 
			
		||||
    DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
 | 
			
		||||
    DEVICE_CLASS_CARBON_DIOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CARBON_MONOXIDE,
 | 
			
		||||
    DEVICE_CLASS_CONDUCTIVITY,
 | 
			
		||||
@@ -117,6 +124,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_DURATION,
 | 
			
		||||
    DEVICE_CLASS_EMPTY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_DISTANCE,
 | 
			
		||||
    DEVICE_CLASS_ENERGY_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_FREQUENCY,
 | 
			
		||||
    DEVICE_CLASS_GAS,
 | 
			
		||||
@@ -138,6 +146,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION,
 | 
			
		||||
    DEVICE_CLASS_PRECIPITATION_INTENSITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_ENERGY,
 | 
			
		||||
    DEVICE_CLASS_REACTIVE_POWER,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    DEVICE_CLASS_SOUND_PRESSURE,
 | 
			
		||||
@@ -153,6 +162,7 @@ DEVICE_CLASSES = [
 | 
			
		||||
    DEVICE_CLASS_VOLUME_STORAGE,
 | 
			
		||||
    DEVICE_CLASS_WATER,
 | 
			
		||||
    DEVICE_CLASS_WEIGHT,
 | 
			
		||||
    DEVICE_CLASS_WIND_DIRECTION,
 | 
			
		||||
    DEVICE_CLASS_WIND_SPEED,
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -174,6 +174,16 @@ AudioPipelineState AudioPipeline::process_state() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
 | 
			
		||||
    xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
 | 
			
		||||
    return AudioPipelineState::ERROR_READING;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
 | 
			
		||||
    xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
 | 
			
		||||
    return AudioPipelineState::ERROR_DECODING;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) &&
 | 
			
		||||
      (!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) &&
 | 
			
		||||
       (event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) {
 | 
			
		||||
@@ -203,16 +213,6 @@ AudioPipelineState AudioPipeline::process_state() {
 | 
			
		||||
    return AudioPipelineState::STOPPED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
 | 
			
		||||
    xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
 | 
			
		||||
    return AudioPipelineState::ERROR_READING;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
 | 
			
		||||
    xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
 | 
			
		||||
    return AudioPipelineState::ERROR_DECODING;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->pause_state_) {
 | 
			
		||||
    return AudioPipelineState::PAUSED;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
from esphome import automation, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from esphome.components import i2c, key_provider
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
@@ -8,13 +8,16 @@ from esphome.const import (
 | 
			
		||||
    CONF_INVERTED,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    CONF_ON_KEY,
 | 
			
		||||
    CONF_OPEN_DRAIN,
 | 
			
		||||
    CONF_OUTPUT,
 | 
			
		||||
    CONF_PULLDOWN,
 | 
			
		||||
    CONF_PULLUP,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_KEYPAD = "keypad"
 | 
			
		||||
CONF_KEYS = "keys"
 | 
			
		||||
CONF_KEY_ROWS = "key_rows"
 | 
			
		||||
CONF_KEY_COLUMNS = "key_columns"
 | 
			
		||||
CONF_SLEEP_TIME = "sleep_time"
 | 
			
		||||
@@ -22,22 +25,47 @@ CONF_SCAN_TIME = "scan_time"
 | 
			
		||||
CONF_DEBOUNCE_TIME = "debounce_time"
 | 
			
		||||
CONF_SX1509_ID = "sx1509_id"
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["key_provider"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
sx1509_ns = cg.esphome_ns.namespace("sx1509")
 | 
			
		||||
 | 
			
		||||
SX1509Component = sx1509_ns.class_("SX1509Component", cg.Component, i2c.I2CDevice)
 | 
			
		||||
SX1509Component = sx1509_ns.class_(
 | 
			
		||||
    "SX1509Component", cg.Component, i2c.I2CDevice, key_provider.KeyProvider
 | 
			
		||||
)
 | 
			
		||||
SX1509GPIOPin = sx1509_ns.class_("SX1509GPIOPin", cg.GPIOPin)
 | 
			
		||||
SX1509KeyTrigger = sx1509_ns.class_(
 | 
			
		||||
    "SX1509KeyTrigger", automation.Trigger.template(cg.uint8)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
KEYPAD_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8),
 | 
			
		||||
        cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8),
 | 
			
		||||
        cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192),
 | 
			
		||||
        cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128),
 | 
			
		||||
        cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
def check_keys(config):
 | 
			
		||||
    if CONF_KEYS in config:
 | 
			
		||||
        if len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "The number of key codes must equal the number of rows * columns"
 | 
			
		||||
            )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
KEYPAD_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_KEY_ROWS): cv.int_range(min=2, max=8),
 | 
			
		||||
            cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8),
 | 
			
		||||
            cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192),
 | 
			
		||||
            cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128),
 | 
			
		||||
            cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64),
 | 
			
		||||
            cv.Optional(CONF_KEYS): cv.string,
 | 
			
		||||
            cv.Optional(CONF_ON_KEY): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SX1509KeyTrigger),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    check_keys,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
@@ -56,17 +84,22 @@ async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
    if CONF_KEYPAD in config:
 | 
			
		||||
        keypad = config[CONF_KEYPAD]
 | 
			
		||||
        cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS]))
 | 
			
		||||
    if conf := config.get(CONF_KEYPAD):
 | 
			
		||||
        cg.add(var.set_rows_cols(conf[CONF_KEY_ROWS], conf[CONF_KEY_COLUMNS]))
 | 
			
		||||
        if (
 | 
			
		||||
            CONF_SLEEP_TIME in keypad
 | 
			
		||||
            and CONF_SCAN_TIME in keypad
 | 
			
		||||
            and CONF_DEBOUNCE_TIME in keypad
 | 
			
		||||
            CONF_SLEEP_TIME in conf
 | 
			
		||||
            and CONF_SCAN_TIME in conf
 | 
			
		||||
            and CONF_DEBOUNCE_TIME in conf
 | 
			
		||||
        ):
 | 
			
		||||
            cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME]))
 | 
			
		||||
            cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME]))
 | 
			
		||||
            cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME]))
 | 
			
		||||
            cg.add(var.set_sleep_time(conf[CONF_SLEEP_TIME]))
 | 
			
		||||
            cg.add(var.set_scan_time(conf[CONF_SCAN_TIME]))
 | 
			
		||||
            cg.add(var.set_debounce_time(conf[CONF_DEBOUNCE_TIME]))
 | 
			
		||||
        if keys := conf.get(CONF_KEYS):
 | 
			
		||||
            cg.add(var.set_keys(keys))
 | 
			
		||||
        for tconf in conf.get(CONF_ON_KEY, []):
 | 
			
		||||
            trigger = cg.new_Pvariable(tconf[CONF_TRIGGER_ID])
 | 
			
		||||
            cg.add(var.register_key_trigger(trigger))
 | 
			
		||||
            await automation.build_automation(trigger, [(cg.uint8, "x")], tconf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_mode(value):
 | 
			
		||||
 
 | 
			
		||||
@@ -48,6 +48,30 @@ void SX1509Component::loop() {
 | 
			
		||||
    uint16_t key_data = this->read_key_data();
 | 
			
		||||
    for (auto *binary_sensor : this->keypad_binary_sensors_)
 | 
			
		||||
      binary_sensor->process(key_data);
 | 
			
		||||
    if (this->keys_.empty())
 | 
			
		||||
      return;
 | 
			
		||||
    if (key_data == 0) {
 | 
			
		||||
      this->last_key_ = 0;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    int row, col;
 | 
			
		||||
    for (row = 0; row < 7; row++) {
 | 
			
		||||
      if (key_data & (1 << row))
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    for (col = 8; col < 15; col++) {
 | 
			
		||||
      if (key_data & (1 << col))
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    col -= 8;
 | 
			
		||||
    uint8_t key = this->keys_[row * this->cols_ + col];
 | 
			
		||||
    if (key == this->last_key_)
 | 
			
		||||
      return;
 | 
			
		||||
    this->last_key_ = key;
 | 
			
		||||
    ESP_LOGV(TAG, "row %d, col %d, key '%c'", row, col, key);
 | 
			
		||||
    for (auto &trigger : this->key_triggers_)
 | 
			
		||||
      trigger->trigger(key);
 | 
			
		||||
    this->send_key_(key);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -230,9 +254,9 @@ void SX1509Component::setup_keypad_() {
 | 
			
		||||
  scan_time_bits &= 0b111;  // Scan time is bits 2:0
 | 
			
		||||
  temp_byte = sleep_time_ | scan_time_bits;
 | 
			
		||||
  this->write_byte(REG_KEY_CONFIG_1, temp_byte);
 | 
			
		||||
  rows_ = (rows_ - 1) & 0b111;  // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc.
 | 
			
		||||
  cols_ = (cols_ - 1) & 0b111;  // 0b000 = 1 column, ob111 = 8 columns, etc.
 | 
			
		||||
  this->write_byte(REG_KEY_CONFIG_2, (rows_ << 3) | cols_);
 | 
			
		||||
  temp_byte = ((this->rows_ - 1) & 0b111) << 3;  // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc.
 | 
			
		||||
  temp_byte |= (this->cols_ - 1) & 0b111;        // 0b000 = 1 column, ob111 = 8 columns, etc.
 | 
			
		||||
  this->write_byte(REG_KEY_CONFIG_2, temp_byte);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t SX1509Component::read_key_data() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/key_provider/key_provider.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "sx1509_gpio_pin.h"
 | 
			
		||||
@@ -27,7 +28,9 @@ class SX1509Processor {
 | 
			
		||||
  virtual void process(uint16_t data){};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SX1509Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
class SX1509KeyTrigger : public Trigger<uint8_t> {};
 | 
			
		||||
 | 
			
		||||
class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider {
 | 
			
		||||
 public:
 | 
			
		||||
  SX1509Component() = default;
 | 
			
		||||
 | 
			
		||||
@@ -47,12 +50,14 @@ class SX1509Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
    this->cols_ = cols;
 | 
			
		||||
    this->has_keypad_ = true;
 | 
			
		||||
  };
 | 
			
		||||
  void set_keys(std::string keys) { this->keys_ = std::move(keys); };
 | 
			
		||||
  void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; };
 | 
			
		||||
  void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; };
 | 
			
		||||
  void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; };
 | 
			
		||||
  void register_keypad_binary_sensor(SX1509Processor *binary_sensor) {
 | 
			
		||||
    this->keypad_binary_sensors_.push_back(binary_sensor);
 | 
			
		||||
  }
 | 
			
		||||
  void register_key_trigger(SX1509KeyTrigger *trig) { this->key_triggers_.push_back(trig); };
 | 
			
		||||
  void setup_led_driver(uint8_t pin);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -65,10 +70,13 @@ class SX1509Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
  bool has_keypad_ = false;
 | 
			
		||||
  uint8_t rows_ = 0;
 | 
			
		||||
  uint8_t cols_ = 0;
 | 
			
		||||
  std::string keys_;
 | 
			
		||||
  uint16_t sleep_time_ = 128;
 | 
			
		||||
  uint8_t scan_time_ = 1;
 | 
			
		||||
  uint8_t debounce_time_ = 1;
 | 
			
		||||
  uint8_t last_key_ = 0;
 | 
			
		||||
  std::vector<SX1509Processor *> keypad_binary_sensors_;
 | 
			
		||||
  std::vector<SX1509KeyTrigger *> key_triggers_;
 | 
			
		||||
 | 
			
		||||
  uint32_t last_loop_timestamp_ = 0;
 | 
			
		||||
  const uint32_t min_loop_period_ = 15;  // ms
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,3 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import mqtt, web_server
 | 
			
		||||
@@ -92,9 +90,9 @@ async def setup_text_core_(
 | 
			
		||||
    var,
 | 
			
		||||
    config,
 | 
			
		||||
    *,
 | 
			
		||||
    min_length: Optional[int],
 | 
			
		||||
    max_length: Optional[int],
 | 
			
		||||
    pattern: Optional[str],
 | 
			
		||||
    min_length: int | None,
 | 
			
		||||
    max_length: int | None,
 | 
			
		||||
    pattern: str | None,
 | 
			
		||||
):
 | 
			
		||||
    await setup_entity(var, config)
 | 
			
		||||
 | 
			
		||||
@@ -121,9 +119,9 @@ async def register_text(
 | 
			
		||||
    var,
 | 
			
		||||
    config,
 | 
			
		||||
    *,
 | 
			
		||||
    min_length: Optional[int] = 0,
 | 
			
		||||
    max_length: Optional[int] = 255,
 | 
			
		||||
    pattern: Optional[str] = None,
 | 
			
		||||
    min_length: int | None = 0,
 | 
			
		||||
    max_length: int | None = 255,
 | 
			
		||||
    pattern: str | None = None,
 | 
			
		||||
):
 | 
			
		||||
    if not CORE.has_id(config[CONF_ID]):
 | 
			
		||||
        var = cg.Pvariable(config[CONF_ID], var)
 | 
			
		||||
@@ -136,9 +134,9 @@ async def register_text(
 | 
			
		||||
async def new_text(
 | 
			
		||||
    config,
 | 
			
		||||
    *,
 | 
			
		||||
    min_length: Optional[int] = 0,
 | 
			
		||||
    max_length: Optional[int] = 255,
 | 
			
		||||
    pattern: Optional[str] = None,
 | 
			
		||||
    min_length: int | None = 0,
 | 
			
		||||
    max_length: int | None = 255,
 | 
			
		||||
    pattern: str | None = None,
 | 
			
		||||
):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await register_text(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
from importlib import resources
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import tzlocal
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +39,7 @@ SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Co
 | 
			
		||||
TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _load_tzdata(iana_key: str) -> Optional[bytes]:
 | 
			
		||||
def _load_tzdata(iana_key: str) -> bytes | None:
 | 
			
		||||
    # From https://tzdata.readthedocs.io/en/latest/#examples
 | 
			
		||||
    try:
 | 
			
		||||
        package_loc, resource = iana_key.rsplit("/", 1)
 | 
			
		||||
 
 | 
			
		||||
@@ -54,8 +54,8 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_select_mappings(list(options_map.keys())))
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_TUYA_ID])
 | 
			
		||||
    cg.add(var.set_tuya_parent(parent))
 | 
			
		||||
    if enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None) is not None:
 | 
			
		||||
    if (enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None)) is not None:
 | 
			
		||||
        cg.add(var.set_select_id(enum_datapoint, False))
 | 
			
		||||
    if int_datapoint := config.get(CONF_INT_DATAPOINT, None) is not None:
 | 
			
		||||
    if (int_datapoint := config.get(CONF_INT_DATAPOINT, None)) is not None:
 | 
			
		||||
        cg.add(var.set_select_id(int_datapoint, True))
 | 
			
		||||
    cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import re
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from esphome import automation, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -322,12 +321,12 @@ def final_validate_device_schema(
 | 
			
		||||
    name: str,
 | 
			
		||||
    *,
 | 
			
		||||
    uart_bus: str = CONF_UART_ID,
 | 
			
		||||
    baud_rate: Optional[int] = None,
 | 
			
		||||
    baud_rate: int | None = None,
 | 
			
		||||
    require_tx: bool = False,
 | 
			
		||||
    require_rx: bool = False,
 | 
			
		||||
    data_bits: Optional[int] = None,
 | 
			
		||||
    parity: Optional[str] = None,
 | 
			
		||||
    stop_bits: Optional[int] = None,
 | 
			
		||||
    data_bits: int | None = None,
 | 
			
		||||
    parity: str | None = None,
 | 
			
		||||
    stop_bits: int | None = None,
 | 
			
		||||
):
 | 
			
		||||
    def validate_baud_rate(value):
 | 
			
		||||
        if value != baud_rate:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										64
									
								
								esphome/components/usb_host/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								esphome/components/usb_host/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,64 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.esp32 import (
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
    add_idf_sdkconfig_option,
 | 
			
		||||
    only_on_variant,
 | 
			
		||||
)
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from esphome.cpp_types import Component
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["bytebuffer"]
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
usb_host_ns = cg.esphome_ns.namespace("usb_host")
 | 
			
		||||
USBHost = usb_host_ns.class_("USBHost", Component)
 | 
			
		||||
USBClient = usb_host_ns.class_("USBClient", Component)
 | 
			
		||||
 | 
			
		||||
CONF_DEVICES = "devices"
 | 
			
		||||
CONF_VID = "vid"
 | 
			
		||||
CONF_PID = "pid"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema:
 | 
			
		||||
    schema = cv.COMPONENT_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(cls),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    if vid:
 | 
			
		||||
        schema = schema.extend({cv.Optional(CONF_VID, default=vid): cv.hex_uint16_t})
 | 
			
		||||
    else:
 | 
			
		||||
        schema = schema.extend({cv.Required(CONF_VID): cv.hex_uint16_t})
 | 
			
		||||
    if pid:
 | 
			
		||||
        schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t})
 | 
			
		||||
    else:
 | 
			
		||||
        schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t})
 | 
			
		||||
    return schema
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.COMPONENT_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(USBHost),
 | 
			
		||||
            cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    cv.only_with_esp_idf,
 | 
			
		||||
    only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3]),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_usb_client(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    for device in config.get(CONF_DEVICES) or ():
 | 
			
		||||
        await register_usb_client(device)
 | 
			
		||||
							
								
								
									
										116
									
								
								esphome/components/usb_host/usb_host.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/usb_host/usb_host.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,116 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
// Should not be needed, but it's required to pass CI clang-tidy checks
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "usb/usb_host.h"
 | 
			
		||||
 | 
			
		||||
#include <list>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_host {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "usb_host";
 | 
			
		||||
 | 
			
		||||
// constants for setup packet type
 | 
			
		||||
static const uint8_t USB_RECIP_DEVICE = 0;
 | 
			
		||||
static const uint8_t USB_RECIP_INTERFACE = 1;
 | 
			
		||||
static const uint8_t USB_RECIP_ENDPOINT = 2;
 | 
			
		||||
static const uint8_t USB_TYPE_STANDARD = 0 << 5;
 | 
			
		||||
static const uint8_t USB_TYPE_CLASS = 1 << 5;
 | 
			
		||||
static const uint8_t USB_TYPE_VENDOR = 2 << 5;
 | 
			
		||||
static const uint8_t USB_DIR_MASK = 1 << 7;
 | 
			
		||||
static const uint8_t USB_DIR_IN = 1 << 7;
 | 
			
		||||
static const uint8_t USB_DIR_OUT = 0;
 | 
			
		||||
static const size_t SETUP_PACKET_SIZE = 8;
 | 
			
		||||
 | 
			
		||||
static const size_t MAX_REQUESTS = 16;  // maximum number of outstanding requests possible.
 | 
			
		||||
 | 
			
		||||
// used to report a transfer status
 | 
			
		||||
struct TransferStatus {
 | 
			
		||||
  bool success;
 | 
			
		||||
  uint16_t error_code;
 | 
			
		||||
  uint8_t *data;
 | 
			
		||||
  size_t data_len;
 | 
			
		||||
  uint8_t endpoint;
 | 
			
		||||
  void *user_data;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
using transfer_cb_t = std::function<void(const TransferStatus &)>;
 | 
			
		||||
 | 
			
		||||
class USBClient;
 | 
			
		||||
 | 
			
		||||
// struct used to capture all data needed for a transfer
 | 
			
		||||
struct TransferRequest {
 | 
			
		||||
  usb_transfer_t *transfer;
 | 
			
		||||
  transfer_cb_t callback;
 | 
			
		||||
  TransferStatus status;
 | 
			
		||||
  USBClient *client;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// callback function type.
 | 
			
		||||
 | 
			
		||||
enum ClientState {
 | 
			
		||||
  USB_CLIENT_INIT = 0,
 | 
			
		||||
  USB_CLIENT_OPEN,
 | 
			
		||||
  USB_CLIENT_CLOSE,
 | 
			
		||||
  USB_CLIENT_GET_DESC,
 | 
			
		||||
  USB_CLIENT_GET_INFO,
 | 
			
		||||
  USB_CLIENT_CONNECTED,
 | 
			
		||||
};
 | 
			
		||||
class USBClient : public Component {
 | 
			
		||||
  friend class USBHost;
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); }
 | 
			
		||||
 | 
			
		||||
  void init_pool() {
 | 
			
		||||
    this->trq_pool_.clear();
 | 
			
		||||
    for (size_t i = 0; i != MAX_REQUESTS; i++)
 | 
			
		||||
      this->trq_pool_.push_back(&this->requests_[i]);
 | 
			
		||||
  }
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  // setup must happen after the host bus has been setup
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::IO; }
 | 
			
		||||
  void on_opened(uint8_t addr);
 | 
			
		||||
  void on_removed(usb_device_handle_t handle);
 | 
			
		||||
  void control_transfer_callback(const usb_transfer_t *xfer) const;
 | 
			
		||||
  void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
 | 
			
		||||
  void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void release_trq(TransferRequest *trq);
 | 
			
		||||
  bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
 | 
			
		||||
                        const std::vector<uint8_t> &data = {});
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool register_();
 | 
			
		||||
  TransferRequest *get_trq_();
 | 
			
		||||
  virtual void disconnect();
 | 
			
		||||
  virtual void on_connected() {}
 | 
			
		||||
  virtual void on_disconnected() { this->init_pool(); }
 | 
			
		||||
 | 
			
		||||
  usb_host_client_handle_t handle_{};
 | 
			
		||||
  usb_device_handle_t device_handle_{};
 | 
			
		||||
  int device_addr_{-1};
 | 
			
		||||
  int state_{USB_CLIENT_INIT};
 | 
			
		||||
  uint16_t vid_{};
 | 
			
		||||
  uint16_t pid_{};
 | 
			
		||||
  std::list<TransferRequest *> trq_pool_{};
 | 
			
		||||
  TransferRequest requests_[MAX_REQUESTS]{};
 | 
			
		||||
};
 | 
			
		||||
class USBHost : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::BUS; }
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<USBClient *> clients_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_host
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										392
									
								
								esphome/components/usb_host/usb_host_client.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										392
									
								
								esphome/components/usb_host/usb_host_client.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,392 @@
 | 
			
		||||
// Should not be needed, but it's required to pass CI clang-tidy checks
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "usb_host.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_host {
 | 
			
		||||
 | 
			
		||||
#pragma GCC diagnostic ignored "-Wparentheses"
 | 
			
		||||
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
static void print_ep_desc(const usb_ep_desc_t *ep_desc) {
 | 
			
		||||
  const char *ep_type_str;
 | 
			
		||||
  int type = ep_desc->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK;
 | 
			
		||||
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case USB_BM_ATTRIBUTES_XFER_CONTROL:
 | 
			
		||||
      ep_type_str = "CTRL";
 | 
			
		||||
      break;
 | 
			
		||||
    case USB_BM_ATTRIBUTES_XFER_ISOC:
 | 
			
		||||
      ep_type_str = "ISOC";
 | 
			
		||||
      break;
 | 
			
		||||
    case USB_BM_ATTRIBUTES_XFER_BULK:
 | 
			
		||||
      ep_type_str = "BULK";
 | 
			
		||||
      break;
 | 
			
		||||
    case USB_BM_ATTRIBUTES_XFER_INT:
 | 
			
		||||
      ep_type_str = "INT";
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      ep_type_str = NULL;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "\t\t*** Endpoint descriptor ***");
 | 
			
		||||
  ESP_LOGV(TAG, "\t\tbLength %d", ep_desc->bLength);
 | 
			
		||||
  ESP_LOGV(TAG, "\t\tbDescriptorType %d", ep_desc->bDescriptorType);
 | 
			
		||||
  ESP_LOGV(TAG, "\t\tbEndpointAddress 0x%x\tEP %d %s", ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc),
 | 
			
		||||
           USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT");
 | 
			
		||||
  ESP_LOGV(TAG, "\t\tbmAttributes 0x%x\t%s", ep_desc->bmAttributes, ep_type_str);
 | 
			
		||||
  ESP_LOGV(TAG, "\t\twMaxPacketSize %d", ep_desc->wMaxPacketSize);
 | 
			
		||||
  ESP_LOGV(TAG, "\t\tbInterval %d", ep_desc->bInterval);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) {
 | 
			
		||||
  ESP_LOGV(TAG, "\t*** Interface descriptor ***");
 | 
			
		||||
  ESP_LOGV(TAG, "\tbLength %d", intf_desc->bLength);
 | 
			
		||||
  ESP_LOGV(TAG, "\tbDescriptorType %d", intf_desc->bDescriptorType);
 | 
			
		||||
  ESP_LOGV(TAG, "\tbInterfaceNumber %d", intf_desc->bInterfaceNumber);
 | 
			
		||||
  ESP_LOGV(TAG, "\tbAlternateSetting %d", intf_desc->bAlternateSetting);
 | 
			
		||||
  ESP_LOGV(TAG, "\tbNumEndpoints %d", intf_desc->bNumEndpoints);
 | 
			
		||||
  ESP_LOGV(TAG, "\tbInterfaceClass 0x%x", intf_desc->bInterfaceProtocol);
 | 
			
		||||
  ESP_LOGV(TAG, "\tiInterface %d", intf_desc->iInterface);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) {
 | 
			
		||||
  ESP_LOGV(TAG, "*** Configuration descriptor ***");
 | 
			
		||||
  ESP_LOGV(TAG, "bLength %d", cfg_desc->bLength);
 | 
			
		||||
  ESP_LOGV(TAG, "bDescriptorType %d", cfg_desc->bDescriptorType);
 | 
			
		||||
  ESP_LOGV(TAG, "wTotalLength %d", cfg_desc->wTotalLength);
 | 
			
		||||
  ESP_LOGV(TAG, "bNumInterfaces %d", cfg_desc->bNumInterfaces);
 | 
			
		||||
  ESP_LOGV(TAG, "bConfigurationValue %d", cfg_desc->bConfigurationValue);
 | 
			
		||||
  ESP_LOGV(TAG, "iConfiguration %d", cfg_desc->iConfiguration);
 | 
			
		||||
  ESP_LOGV(TAG, "bmAttributes 0x%x", cfg_desc->bmAttributes);
 | 
			
		||||
  ESP_LOGV(TAG, "bMaxPower %dmA", cfg_desc->bMaxPower * 2);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) {
 | 
			
		||||
  if (devc_desc == NULL) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "*** Device descriptor ***");
 | 
			
		||||
  ESP_LOGV(TAG, "bLength %d", devc_desc->bLength);
 | 
			
		||||
  ESP_LOGV(TAG, "bDescriptorType %d", devc_desc->bDescriptorType);
 | 
			
		||||
  ESP_LOGV(TAG, "bcdUSB %d.%d0", ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF));
 | 
			
		||||
  ESP_LOGV(TAG, "bDeviceClass 0x%x", devc_desc->bDeviceClass);
 | 
			
		||||
  ESP_LOGV(TAG, "bDeviceSubClass 0x%x", devc_desc->bDeviceSubClass);
 | 
			
		||||
  ESP_LOGV(TAG, "bDeviceProtocol 0x%x", devc_desc->bDeviceProtocol);
 | 
			
		||||
  ESP_LOGV(TAG, "bMaxPacketSize0 %d", devc_desc->bMaxPacketSize0);
 | 
			
		||||
  ESP_LOGV(TAG, "idVendor 0x%x", devc_desc->idVendor);
 | 
			
		||||
  ESP_LOGV(TAG, "idProduct 0x%x", devc_desc->idProduct);
 | 
			
		||||
  ESP_LOGV(TAG, "bcdDevice %d.%d0", ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF));
 | 
			
		||||
  ESP_LOGV(TAG, "iManufacturer %d", devc_desc->iManufacturer);
 | 
			
		||||
  ESP_LOGV(TAG, "iProduct %d", devc_desc->iProduct);
 | 
			
		||||
  ESP_LOGV(TAG, "iSerialNumber %d", devc_desc->iSerialNumber);
 | 
			
		||||
  ESP_LOGV(TAG, "bNumConfigurations %d", devc_desc->bNumConfigurations);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc,
 | 
			
		||||
                                        print_class_descriptor_cb class_specific_cb) {
 | 
			
		||||
  if (cfg_desc == nullptr) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int offset = 0;
 | 
			
		||||
  uint16_t w_total_length = cfg_desc->wTotalLength;
 | 
			
		||||
  const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *) cfg_desc;
 | 
			
		||||
 | 
			
		||||
  do {
 | 
			
		||||
    switch (next_desc->bDescriptorType) {
 | 
			
		||||
      case USB_W_VALUE_DT_CONFIG:
 | 
			
		||||
        usbh_print_cfg_desc((const usb_config_desc_t *) next_desc);
 | 
			
		||||
        break;
 | 
			
		||||
      case USB_W_VALUE_DT_INTERFACE:
 | 
			
		||||
        usbh_print_intf_desc((const usb_intf_desc_t *) next_desc);
 | 
			
		||||
        break;
 | 
			
		||||
      case USB_W_VALUE_DT_ENDPOINT:
 | 
			
		||||
        print_ep_desc((const usb_ep_desc_t *) next_desc);
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        if (class_specific_cb) {
 | 
			
		||||
          class_specific_cb(next_desc);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    next_desc = usb_parse_next_descriptor(next_desc, w_total_length, &offset);
 | 
			
		||||
 | 
			
		||||
  } while (next_desc != NULL);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
 | 
			
		||||
  char buffer[256];
 | 
			
		||||
  if (desc == nullptr)
 | 
			
		||||
    return "(unknown)";
 | 
			
		||||
  char *p = buffer;
 | 
			
		||||
  for (size_t i = 0; i != desc->bLength / 2; i++) {
 | 
			
		||||
    auto c = desc->wData[i];
 | 
			
		||||
    if (c < 0x100)
 | 
			
		||||
      *p++ = static_cast<char>(c);
 | 
			
		||||
  }
 | 
			
		||||
  *p = '\0';
 | 
			
		||||
  return {buffer};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
 | 
			
		||||
  auto *client = static_cast<USBClient *>(ptr);
 | 
			
		||||
  switch (event_msg->event) {
 | 
			
		||||
    case USB_HOST_CLIENT_EVENT_NEW_DEV: {
 | 
			
		||||
      auto addr = event_msg->new_dev.address;
 | 
			
		||||
      ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
 | 
			
		||||
      client->on_opened(addr);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case USB_HOST_CLIENT_EVENT_DEV_GONE: {
 | 
			
		||||
      client->on_removed(event_msg->dev_gone.dev_hdl);
 | 
			
		||||
      ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBClient::setup() {
 | 
			
		||||
  usb_host_client_config_t config{.is_synchronous = false,
 | 
			
		||||
                                  .max_num_event_msg = 5,
 | 
			
		||||
                                  .async = {.client_event_callback = client_event_cb, .callback_arg = this}};
 | 
			
		||||
  auto err = usb_host_client_register(&config, &this->handle_);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err));
 | 
			
		||||
    this->status_set_error("Client register failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  for (auto trq : this->trq_pool_) {
 | 
			
		||||
    usb_host_transfer_alloc(64, 0, &trq->transfer);
 | 
			
		||||
    trq->client = this;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "client setup complete");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBClient::loop() {
 | 
			
		||||
  switch (this->state_) {
 | 
			
		||||
    case USB_CLIENT_OPEN: {
 | 
			
		||||
      int err;
 | 
			
		||||
      ESP_LOGD(TAG, "Open device %d", this->device_addr_);
 | 
			
		||||
      err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
 | 
			
		||||
      if (err != ESP_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
 | 
			
		||||
        this->state_ = USB_CLIENT_INIT;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
 | 
			
		||||
      const usb_device_desc_t *desc;
 | 
			
		||||
      err = usb_host_get_device_descriptor(this->device_handle_, &desc);
 | 
			
		||||
      if (err != ESP_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
 | 
			
		||||
        this->disconnect();
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
 | 
			
		||||
        if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
 | 
			
		||||
          usb_device_info_t dev_info;
 | 
			
		||||
          if ((err = usb_host_device_info(this->device_handle_, &dev_info)) != ESP_OK) {
 | 
			
		||||
            ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
 | 
			
		||||
            this->disconnect();
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          this->state_ = USB_CLIENT_CONNECTED;
 | 
			
		||||
          ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
 | 
			
		||||
                   get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
 | 
			
		||||
                   get_descriptor_string(dev_info.str_desc_product).c_str(),
 | 
			
		||||
                   get_descriptor_string(dev_info.str_desc_serial_num).c_str());
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
          const usb_device_desc_t *device_desc;
 | 
			
		||||
          err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
 | 
			
		||||
          if (err == ESP_OK)
 | 
			
		||||
            usb_client_print_device_descriptor(device_desc);
 | 
			
		||||
          const usb_config_desc_t *config_desc;
 | 
			
		||||
          err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
 | 
			
		||||
          if (err == ESP_OK)
 | 
			
		||||
            usb_client_print_config_descriptor(config_desc, nullptr);
 | 
			
		||||
#endif
 | 
			
		||||
          this->on_connected();
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGD(TAG, "Not our device, closing");
 | 
			
		||||
          this->disconnect();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      usb_host_client_handle_events(this->handle_, 0);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBClient::on_opened(uint8_t addr) {
 | 
			
		||||
  if (this->state_ == USB_CLIENT_INIT) {
 | 
			
		||||
    this->device_addr_ = addr;
 | 
			
		||||
    this->state_ = USB_CLIENT_OPEN;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBClient::on_removed(usb_device_handle_t handle) {
 | 
			
		||||
  if (this->device_handle_ == handle) {
 | 
			
		||||
    this->disconnect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void control_callback(const usb_transfer_t *xfer) {
 | 
			
		||||
  auto *trq = static_cast<TransferRequest *>(xfer->context);
 | 
			
		||||
  trq->status.error_code = xfer->status;
 | 
			
		||||
  trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
 | 
			
		||||
  trq->status.endpoint = xfer->bEndpointAddress;
 | 
			
		||||
  trq->status.data = xfer->data_buffer;
 | 
			
		||||
  trq->status.data_len = xfer->actual_num_bytes;
 | 
			
		||||
  if (trq->callback != nullptr)
 | 
			
		||||
    trq->callback(trq->status);
 | 
			
		||||
  trq->client->release_trq(trq);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
TransferRequest *USBClient::get_trq_() {
 | 
			
		||||
  if (this->trq_pool_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Too many requests queued");
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
  auto *trq = this->trq_pool_.front();
 | 
			
		||||
  this->trq_pool_.pop_front();
 | 
			
		||||
  trq->client = this;
 | 
			
		||||
  trq->transfer->context = trq;
 | 
			
		||||
  trq->transfer->device_handle = this->device_handle_;
 | 
			
		||||
  return trq;
 | 
			
		||||
}
 | 
			
		||||
void USBClient::disconnect() {
 | 
			
		||||
  this->on_disconnected();
 | 
			
		||||
  auto err = usb_host_device_close(this->handle_, this->device_handle_);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Device close failed: %s", esp_err_to_name(err));
 | 
			
		||||
  }
 | 
			
		||||
  this->state_ = USB_CLIENT_INIT;
 | 
			
		||||
  this->device_handle_ = nullptr;
 | 
			
		||||
  this->device_addr_ = -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
 | 
			
		||||
                                 const transfer_cb_t &callback, const std::vector<uint8_t> &data) {
 | 
			
		||||
  auto *trq = this->get_trq_();
 | 
			
		||||
  if (trq == nullptr)
 | 
			
		||||
    return false;
 | 
			
		||||
  auto length = data.size();
 | 
			
		||||
  if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) {
 | 
			
		||||
    ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length,
 | 
			
		||||
             sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t));
 | 
			
		||||
    this->release_trq(trq);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  auto control_packet = ByteBuffer(SETUP_PACKET_SIZE, LITTLE);
 | 
			
		||||
  control_packet.put_uint8(type);
 | 
			
		||||
  control_packet.put_uint8(request);
 | 
			
		||||
  control_packet.put_uint16(value);
 | 
			
		||||
  control_packet.put_uint16(index);
 | 
			
		||||
  control_packet.put_uint16(length);
 | 
			
		||||
  memcpy(trq->transfer->data_buffer, control_packet.get_data().data(), SETUP_PACKET_SIZE);
 | 
			
		||||
  if (length != 0 && !(type & USB_DIR_IN)) {
 | 
			
		||||
    memcpy(trq->transfer->data_buffer + SETUP_PACKET_SIZE, data.data(), length);
 | 
			
		||||
  }
 | 
			
		||||
  trq->callback = callback;
 | 
			
		||||
  trq->transfer->bEndpointAddress = type & USB_DIR_MASK;
 | 
			
		||||
  trq->transfer->num_bytes = static_cast<int>(length + SETUP_PACKET_SIZE);
 | 
			
		||||
  trq->transfer->callback = reinterpret_cast<usb_transfer_cb_t>(control_callback);
 | 
			
		||||
  auto err = usb_host_transfer_submit_control(this->handle_, trq->transfer);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to submit control transfer, err=%s", esp_err_to_name(err));
 | 
			
		||||
    this->release_trq(trq);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void transfer_callback(usb_transfer_t *xfer) {
 | 
			
		||||
  auto *trq = static_cast<TransferRequest *>(xfer->context);
 | 
			
		||||
  trq->status.error_code = xfer->status;
 | 
			
		||||
  trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
 | 
			
		||||
  trq->status.endpoint = xfer->bEndpointAddress;
 | 
			
		||||
  trq->status.data = xfer->data_buffer;
 | 
			
		||||
  trq->status.data_len = xfer->actual_num_bytes;
 | 
			
		||||
  if (trq->callback != nullptr)
 | 
			
		||||
    trq->callback(trq->status);
 | 
			
		||||
  trq->client->release_trq(trq);
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Performs a transfer input operation.
 | 
			
		||||
 *
 | 
			
		||||
 * @param ep_address The endpoint address.
 | 
			
		||||
 * @param callback The callback function to be called when the transfer is complete.
 | 
			
		||||
 * @param length The length of the data to be transferred.
 | 
			
		||||
 *
 | 
			
		||||
 * @throws None.
 | 
			
		||||
 */
 | 
			
		||||
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
 | 
			
		||||
  auto trq = this->get_trq_();
 | 
			
		||||
  if (trq == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Too many requests queued");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  trq->callback = callback;
 | 
			
		||||
  trq->transfer->callback = transfer_callback;
 | 
			
		||||
  trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN;
 | 
			
		||||
  trq->transfer->num_bytes = length;
 | 
			
		||||
  auto err = usb_host_transfer_submit(trq->transfer);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
 | 
			
		||||
    this->release_trq(trq);
 | 
			
		||||
    this->disconnect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Performs an output transfer operation.
 | 
			
		||||
 *
 | 
			
		||||
 * @param ep_address The endpoint address.
 | 
			
		||||
 * @param callback The callback function to be called when the transfer is complete.
 | 
			
		||||
 * @param data The data to be transferred.
 | 
			
		||||
 * @param length The length of the data to be transferred.
 | 
			
		||||
 *
 | 
			
		||||
 * @throws None.
 | 
			
		||||
 */
 | 
			
		||||
void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
 | 
			
		||||
  auto trq = this->get_trq_();
 | 
			
		||||
  if (trq == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Too many requests queued");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  trq->callback = callback;
 | 
			
		||||
  trq->transfer->callback = transfer_callback;
 | 
			
		||||
  trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT;
 | 
			
		||||
  trq->transfer->num_bytes = length;
 | 
			
		||||
  memcpy(trq->transfer->data_buffer, data, length);
 | 
			
		||||
  auto err = usb_host_transfer_submit(trq->transfer);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
 | 
			
		||||
    this->release_trq(trq);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBClient::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "USBClient");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Vendor id %04X", this->vid_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Product id %04X", this->pid_);
 | 
			
		||||
}
 | 
			
		||||
void USBClient::release_trq(TransferRequest *trq) { this->trq_pool_.push_back(trq); }
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_host
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										35
									
								
								esphome/components/usb_host/usb_host_component.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/usb_host/usb_host_component.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
// Should not be needed, but it's required to pass CI clang-tidy checks
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "usb_host.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_host {
 | 
			
		||||
 | 
			
		||||
void USBHost::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setup starts");
 | 
			
		||||
  usb_host_config_t config{};
 | 
			
		||||
 | 
			
		||||
  if (usb_host_install(&config) != ESP_OK) {
 | 
			
		||||
    this->status_set_error("usb_host_install failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBHost::loop() {
 | 
			
		||||
  int err;
 | 
			
		||||
  uint32_t event_flags;
 | 
			
		||||
  err = usb_host_lib_handle_events(0, &event_flags);
 | 
			
		||||
  if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
 | 
			
		||||
    ESP_LOGD(TAG, "lib_handle_events failed failed: %s", esp_err_to_name(err));
 | 
			
		||||
  }
 | 
			
		||||
  if (event_flags != 0) {
 | 
			
		||||
    ESP_LOGD(TAG, "Event flags %" PRIu32 "X", event_flags);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_host
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										134
									
								
								esphome/components/usb_uart/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								esphome/components/usb_uart/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,134 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.uart import (
 | 
			
		||||
    CONF_DATA_BITS,
 | 
			
		||||
    CONF_PARITY,
 | 
			
		||||
    CONF_STOP_BITS,
 | 
			
		||||
    UARTComponent,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.usb_host import register_usb_client, usb_device_schema
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BAUD_RATE,
 | 
			
		||||
    CONF_BUFFER_SIZE,
 | 
			
		||||
    CONF_CHANNELS,
 | 
			
		||||
    CONF_DEBUG,
 | 
			
		||||
    CONF_DUMMY_RECEIVER,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
)
 | 
			
		||||
from esphome.cpp_types import Component
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["uart", "usb_host", "bytebuffer"]
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
 | 
			
		||||
usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
 | 
			
		||||
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
 | 
			
		||||
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
 | 
			
		||||
UART_PARITY_OPTIONS = {
 | 
			
		||||
    "NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
 | 
			
		||||
    "EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN,
 | 
			
		||||
    "ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD,
 | 
			
		||||
    "MARK": UARTParityOptions.UART_CONFIG_PARITY_MARK,
 | 
			
		||||
    "SPACE": UARTParityOptions.UART_CONFIG_PARITY_SPACE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
UARTStopBitsOptions = usb_uart_ns.enum("UARTStopBitsOptions")
 | 
			
		||||
UART_STOP_BITS_OPTIONS = {
 | 
			
		||||
    "1": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1,
 | 
			
		||||
    "1.5": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1_5,
 | 
			
		||||
    "2": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
DEFAULT_BAUD_RATE = 9600
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Type:
 | 
			
		||||
    def __init__(self, name, vid, pid, cls, max_channels=1, baud_rate_required=True):
 | 
			
		||||
        self.name = name
 | 
			
		||||
        cls = cls or name
 | 
			
		||||
        self.vid = vid
 | 
			
		||||
        self.pid = pid
 | 
			
		||||
        self.cls = usb_uart_ns.class_(f"USBUartType{cls}", USBUartComponent)
 | 
			
		||||
        self.max_channels = max_channels
 | 
			
		||||
        self.baud_rate_required = baud_rate_required
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
uart_types = (
 | 
			
		||||
    Type("CH34X", 0x1A86, 0x55D5, "CH34X", 3),
 | 
			
		||||
    Type("CH340", 0x1A86, 0x7523, "CH34X", 1),
 | 
			
		||||
    Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False),
 | 
			
		||||
    Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False),
 | 
			
		||||
    Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False),
 | 
			
		||||
    Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def channel_schema(channels, baud_rate_required):
 | 
			
		||||
    return cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_CHANNELS): cv.All(
 | 
			
		||||
                cv.ensure_list(
 | 
			
		||||
                    cv.Schema(
 | 
			
		||||
                        {
 | 
			
		||||
                            cv.GenerateID(): cv.declare_id(USBUartChannel),
 | 
			
		||||
                            cv.Optional(CONF_BUFFER_SIZE, default=256): cv.int_range(
 | 
			
		||||
                                min=64, max=8192
 | 
			
		||||
                            ),
 | 
			
		||||
                            (
 | 
			
		||||
                                cv.Required(CONF_BAUD_RATE)
 | 
			
		||||
                                if baud_rate_required
 | 
			
		||||
                                else cv.Optional(
 | 
			
		||||
                                    CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE
 | 
			
		||||
                                )
 | 
			
		||||
                            ): cv.int_range(min=300, max=1000000),
 | 
			
		||||
                            cv.Optional(CONF_STOP_BITS, default="1"): cv.enum(
 | 
			
		||||
                                UART_STOP_BITS_OPTIONS, upper=True
 | 
			
		||||
                            ),
 | 
			
		||||
                            cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
 | 
			
		||||
                                UART_PARITY_OPTIONS, upper=True
 | 
			
		||||
                            ),
 | 
			
		||||
                            cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(
 | 
			
		||||
                                min=5, max=8
 | 
			
		||||
                            ),
 | 
			
		||||
                            cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean,
 | 
			
		||||
                            cv.Optional(CONF_DEBUG, default=False): cv.boolean,
 | 
			
		||||
                        }
 | 
			
		||||
                    )
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Length(max=channels),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.ensure_list(
 | 
			
		||||
    cv.typed_schema(
 | 
			
		||||
        {
 | 
			
		||||
            it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
 | 
			
		||||
                channel_schema(it.max_channels, it.baud_rate_required)
 | 
			
		||||
            )
 | 
			
		||||
            for it in uart_types
 | 
			
		||||
        },
 | 
			
		||||
        upper=True,
 | 
			
		||||
    )
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    for device in config:
 | 
			
		||||
        var = await register_usb_client(device)
 | 
			
		||||
        for index, channel in enumerate(device[CONF_CHANNELS]):
 | 
			
		||||
            chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE])
 | 
			
		||||
            await cg.register_parented(chvar, var)
 | 
			
		||||
            cg.add(chvar.set_rx_buffer_size(channel[CONF_BUFFER_SIZE]))
 | 
			
		||||
            cg.add(chvar.set_stop_bits(channel[CONF_STOP_BITS]))
 | 
			
		||||
            cg.add(chvar.set_data_bits(channel[CONF_DATA_BITS]))
 | 
			
		||||
            cg.add(chvar.set_parity(channel[CONF_PARITY]))
 | 
			
		||||
            cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE]))
 | 
			
		||||
            cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER]))
 | 
			
		||||
            cg.add(chvar.set_debug(channel[CONF_DEBUG]))
 | 
			
		||||
            cg.add(var.add_channel(chvar))
 | 
			
		||||
            if channel[CONF_DEBUG]:
 | 
			
		||||
                cg.add_define("USE_UART_DEBUGGER")
 | 
			
		||||
							
								
								
									
										80
									
								
								esphome/components/usb_uart/ch34x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/usb_uart/ch34x.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "usb_uart.h"
 | 
			
		||||
#include "usb/usb_host.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_uart {
 | 
			
		||||
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
/**
 | 
			
		||||
 * CH34x
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
void USBUartTypeCH34X::enable_channels() {
 | 
			
		||||
  // enable the channels
 | 
			
		||||
  for (auto channel : this->channels_) {
 | 
			
		||||
    if (!channel->initialised_)
 | 
			
		||||
      continue;
 | 
			
		||||
    usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
 | 
			
		||||
      if (!status.success) {
 | 
			
		||||
        ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
 | 
			
		||||
        channel->initialised_ = false;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    uint8_t divisor = 7;
 | 
			
		||||
    uint32_t clk = 12000000;
 | 
			
		||||
 | 
			
		||||
    auto baud_rate = channel->baud_rate_;
 | 
			
		||||
    if (baud_rate < 256000) {
 | 
			
		||||
      if (baud_rate > 6000000 / 255) {
 | 
			
		||||
        divisor = 3;
 | 
			
		||||
        clk = 6000000;
 | 
			
		||||
      } else if (baud_rate > 750000 / 255) {
 | 
			
		||||
        divisor = 2;
 | 
			
		||||
        clk = 750000;
 | 
			
		||||
      } else if (baud_rate > 93750 / 255) {
 | 
			
		||||
        divisor = 1;
 | 
			
		||||
        clk = 93750;
 | 
			
		||||
      } else {
 | 
			
		||||
        divisor = 0;
 | 
			
		||||
        clk = 11719;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGV(TAG, "baud_rate: %" PRIu32 ", divisor: %d, clk: %" PRIu32, baud_rate, divisor, clk);
 | 
			
		||||
    auto factor = static_cast<uint8_t>(clk / baud_rate);
 | 
			
		||||
    if (factor == 0 || factor == 0xFF) {
 | 
			
		||||
      ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate);
 | 
			
		||||
      channel->initialised_ = false;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1)))
 | 
			
		||||
      factor++;
 | 
			
		||||
    factor = 256 - factor;
 | 
			
		||||
 | 
			
		||||
    uint16_t value = 0xC0;
 | 
			
		||||
    if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2)
 | 
			
		||||
      value |= 4;
 | 
			
		||||
    switch (channel->parity_) {
 | 
			
		||||
      case UART_CONFIG_PARITY_NONE:
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        value |= 8 | ((channel->parity_ - 1) << 4);
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    value |= channel->data_bits_ - 5;
 | 
			
		||||
    value <<= 8;
 | 
			
		||||
    value |= 0x8C;
 | 
			
		||||
    uint8_t cmd = 0xA1 + channel->index_;
 | 
			
		||||
    if (channel->index_ >= 2)
 | 
			
		||||
      cmd += 0xE;
 | 
			
		||||
    this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback);
 | 
			
		||||
  }
 | 
			
		||||
  USBUartTypeCdcAcm::enable_channels();
 | 
			
		||||
}
 | 
			
		||||
}  // namespace usb_uart
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										126
									
								
								esphome/components/usb_uart/cp210x.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								esphome/components/usb_uart/cp210x.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,126 @@
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "usb_uart.h"
 | 
			
		||||
#include "usb/usb_host.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_uart {
 | 
			
		||||
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
/**
 | 
			
		||||
 * Silabs CP210x Commands
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
static constexpr uint8_t IFC_ENABLE = 0x00;       // Enable or disable the interface.
 | 
			
		||||
static constexpr uint8_t SET_BAUDDIV = 0x01;      // Set the baud rate divisor.
 | 
			
		||||
static constexpr uint8_t GET_BAUDDIV = 0x02;      // Get the baud rate divisor.
 | 
			
		||||
static constexpr uint8_t SET_LINE_CTL = 0x03;     // Set the line control.
 | 
			
		||||
static constexpr uint8_t GET_LINE_CTL = 0x04;     // Get the line control.
 | 
			
		||||
static constexpr uint8_t SET_BREAK = 0x05;        // Set a BREAK.
 | 
			
		||||
static constexpr uint8_t IMM_CHAR = 0x06;         // Send character out of order.
 | 
			
		||||
static constexpr uint8_t SET_MHS = 0x07;          // Set modem handshaking.
 | 
			
		||||
static constexpr uint8_t GET_MDMSTS = 0x08;       // Get modem status.
 | 
			
		||||
static constexpr uint8_t SET_XON = 0x09;          // Emulate XON.
 | 
			
		||||
static constexpr uint8_t SET_XOFF = 0x0A;         // Emulate XOFF.
 | 
			
		||||
static constexpr uint8_t SET_EVENTMASK = 0x0B;    // Set the event mask.
 | 
			
		||||
static constexpr uint8_t GET_EVENTMASK = 0x0C;    // Get the event mask.
 | 
			
		||||
static constexpr uint8_t GET_EVENTSTATE = 0x16;   // Get the event state.
 | 
			
		||||
static constexpr uint8_t SET_RECEIVE = 0x17;      // Set receiver max timeout.
 | 
			
		||||
static constexpr uint8_t GET_RECEIVE = 0x18;      // Get receiver max timeout.
 | 
			
		||||
static constexpr uint8_t SET_CHAR = 0x0D;         // Set special character individually.
 | 
			
		||||
static constexpr uint8_t GET_CHARS = 0x0E;        // Get special characters.
 | 
			
		||||
static constexpr uint8_t GET_PROPS = 0x0F;        // Get properties.
 | 
			
		||||
static constexpr uint8_t GET_COMM_STATUS = 0x10;  // Get the serial status.
 | 
			
		||||
static constexpr uint8_t RESET = 0x11;            // Reset.
 | 
			
		||||
static constexpr uint8_t PURGE = 0x12;            // Purge.
 | 
			
		||||
static constexpr uint8_t SET_FLOW = 0x13;         // Set flow control.
 | 
			
		||||
static constexpr uint8_t GET_FLOW = 0x14;         // Get flow control.
 | 
			
		||||
static constexpr uint8_t EMBED_EVENTS = 0x15;     // Control embedding of events in the data stream.
 | 
			
		||||
static constexpr uint8_t GET_BAUDRATE = 0x1D;     // Get the baud rate.
 | 
			
		||||
static constexpr uint8_t SET_BAUDRATE = 0x1E;     // Set the baud rate.
 | 
			
		||||
static constexpr uint8_t SET_CHARS = 0x19;        // Set special characters.
 | 
			
		||||
static constexpr uint8_t VENDOR_SPECIFIC = 0xFF;  // Vendor specific command.
 | 
			
		||||
 | 
			
		||||
std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors_(usb_device_handle_t dev_hdl) {
 | 
			
		||||
  const usb_config_desc_t *config_desc;
 | 
			
		||||
  const usb_device_desc_t *device_desc;
 | 
			
		||||
  int conf_offset = 0, ep_offset;
 | 
			
		||||
  std::vector<CdcEps> cdc_devs{};
 | 
			
		||||
 | 
			
		||||
  // Get required descriptors
 | 
			
		||||
  if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "get_device_descriptor failed");
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "get_active_config_descriptor failed");
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u", device_desc->bDeviceClass, device_desc->bDeviceSubClass);
 | 
			
		||||
  ESP_LOGD(TAG, "bNumInterfaces: %u", config_desc->bNumInterfaces);
 | 
			
		||||
  if (device_desc->bDeviceClass != 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "bDeviceClass != 0");
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) {
 | 
			
		||||
    auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset);
 | 
			
		||||
    if (!data_desc) {
 | 
			
		||||
      ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    if (data_desc->bNumEndpoints != 2 || data_desc->bInterfaceClass != USB_CLASS_VENDOR_SPEC) {
 | 
			
		||||
      ESP_LOGE(TAG, "data_desc: bInterfaceClass == %u, bInterfaceSubClass == %u, bNumEndpoints == %u",
 | 
			
		||||
               data_desc->bInterfaceClass, data_desc->bInterfaceSubClass, data_desc->bNumEndpoints);
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    ep_offset = conf_offset;
 | 
			
		||||
    auto out_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 0, config_desc->wTotalLength, &ep_offset);
 | 
			
		||||
    if (!out_ep) {
 | 
			
		||||
      ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed");
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    ep_offset = conf_offset;
 | 
			
		||||
    auto in_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 1, config_desc->wTotalLength, &ep_offset);
 | 
			
		||||
    if (!in_ep) {
 | 
			
		||||
      ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
    if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
 | 
			
		||||
      cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
 | 
			
		||||
    } else {
 | 
			
		||||
      cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return cdc_devs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartTypeCP210X::enable_channels() {
 | 
			
		||||
  // enable the channels
 | 
			
		||||
  for (auto channel : this->channels_) {
 | 
			
		||||
    if (!channel->initialised_)
 | 
			
		||||
      continue;
 | 
			
		||||
    usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
 | 
			
		||||
      if (!status.success) {
 | 
			
		||||
        ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
 | 
			
		||||
        channel->initialised_ = false;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
    this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback);
 | 
			
		||||
    uint16_t line_control = channel->stop_bits_;
 | 
			
		||||
    line_control |= static_cast<uint8_t>(channel->parity_) << 4;
 | 
			
		||||
    line_control |= channel->data_bits_ << 8;
 | 
			
		||||
    ESP_LOGD(TAG, "Line control value 0x%X", line_control);
 | 
			
		||||
    this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_,
 | 
			
		||||
                           callback);
 | 
			
		||||
    auto baud = ByteBuffer::wrap(channel->baud_rate_, LITTLE);
 | 
			
		||||
    this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback,
 | 
			
		||||
                           baud.get_data());
 | 
			
		||||
  }
 | 
			
		||||
  USBUartTypeCdcAcm::enable_channels();
 | 
			
		||||
}
 | 
			
		||||
}  // namespace usb_uart
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										325
									
								
								esphome/components/usb_uart/usb_uart.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										325
									
								
								esphome/components/usb_uart/usb_uart.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,325 @@
 | 
			
		||||
// Should not be needed, but it's required to pass CI clang-tidy checks
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "usb_uart.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/uart/uart_debugger.h"
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_uart {
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 * Given a configuration, look for the required interfaces defining a CDC-ACM device
 | 
			
		||||
 * @param config_desc The configuration descriptor
 | 
			
		||||
 * @param intf_idx The index of the interface to be examined
 | 
			
		||||
 * @return
 | 
			
		||||
 */
 | 
			
		||||
static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
 | 
			
		||||
  int conf_offset, ep_offset;
 | 
			
		||||
  const usb_ep_desc_t *notify_ep{}, *in_ep{}, *out_ep{};
 | 
			
		||||
  uint8_t interface_number = 0;
 | 
			
		||||
  // look for an interface with one interrupt endpoint (notify), and an interface with two bulk endpoints (data in/out)
 | 
			
		||||
  for (;;) {
 | 
			
		||||
    auto intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
 | 
			
		||||
    if (!intf_desc) {
 | 
			
		||||
      ESP_LOGE(TAG, "usb_parse_interface_descriptor failed");
 | 
			
		||||
      return nullopt;
 | 
			
		||||
    }
 | 
			
		||||
    if (intf_desc->bNumEndpoints == 1) {
 | 
			
		||||
      ep_offset = conf_offset;
 | 
			
		||||
      notify_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
 | 
			
		||||
      if (!notify_ep) {
 | 
			
		||||
        ESP_LOGE(TAG, "notify_ep: usb_parse_endpoint_descriptor_by_index failed");
 | 
			
		||||
        return nullopt;
 | 
			
		||||
      }
 | 
			
		||||
      if (notify_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_INT)
 | 
			
		||||
        notify_ep = nullptr;
 | 
			
		||||
    } else if (USB_CLASS_CDC_DATA && intf_desc->bNumEndpoints == 2) {
 | 
			
		||||
      interface_number = intf_desc->bInterfaceNumber;
 | 
			
		||||
      ep_offset = conf_offset;
 | 
			
		||||
      out_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
 | 
			
		||||
      if (!out_ep) {
 | 
			
		||||
        ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed");
 | 
			
		||||
        return nullopt;
 | 
			
		||||
      }
 | 
			
		||||
      if (out_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
 | 
			
		||||
        out_ep = nullptr;
 | 
			
		||||
      ep_offset = conf_offset;
 | 
			
		||||
      in_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 1, config_desc->wTotalLength, &ep_offset);
 | 
			
		||||
      if (!in_ep) {
 | 
			
		||||
        ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
 | 
			
		||||
        return nullopt;
 | 
			
		||||
      }
 | 
			
		||||
      if (in_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
 | 
			
		||||
        in_ep = nullptr;
 | 
			
		||||
    }
 | 
			
		||||
    if (in_ep != nullptr && out_ep != nullptr && notify_ep != nullptr)
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN)
 | 
			
		||||
    return CdcEps{notify_ep, in_ep, out_ep, interface_number};
 | 
			
		||||
  return CdcEps{notify_ep, out_ep, in_ep, interface_number};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t dev_hdl) {
 | 
			
		||||
  const usb_config_desc_t *config_desc;
 | 
			
		||||
  const usb_device_desc_t *device_desc;
 | 
			
		||||
  int desc_offset = 0;
 | 
			
		||||
  std::vector<CdcEps> cdc_devs{};
 | 
			
		||||
 | 
			
		||||
  // Get required descriptors
 | 
			
		||||
  if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "get_device_descriptor failed");
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "get_active_config_descriptor failed");
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  if (device_desc->bDeviceClass == USB_CLASS_COMM) {
 | 
			
		||||
    // single CDC-ACM device
 | 
			
		||||
    if (auto eps = get_cdc(config_desc, 0)) {
 | 
			
		||||
      ESP_LOGV(TAG, "Found CDC-ACM device");
 | 
			
		||||
      cdc_devs.push_back(*eps);
 | 
			
		||||
    }
 | 
			
		||||
    return cdc_devs;
 | 
			
		||||
  }
 | 
			
		||||
  if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) &&
 | 
			
		||||
       (device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) ||
 | 
			
		||||
      ((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) &&
 | 
			
		||||
       (device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) {
 | 
			
		||||
    // This is a composite device, that uses Interface Association Descriptor
 | 
			
		||||
    const auto *this_desc = reinterpret_cast<const usb_standard_desc_t *>(config_desc);
 | 
			
		||||
    for (;;) {
 | 
			
		||||
      this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength,
 | 
			
		||||
                                                    USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset);
 | 
			
		||||
      if (!this_desc)
 | 
			
		||||
        break;
 | 
			
		||||
      const auto *iad_desc = reinterpret_cast<const usb_iad_desc_t *>(this_desc);
 | 
			
		||||
 | 
			
		||||
      if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) {
 | 
			
		||||
        ESP_LOGV(TAG, "Found CDC-ACM device in composite device");
 | 
			
		||||
        if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface))
 | 
			
		||||
          cdc_devs.push_back(*eps);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return cdc_devs;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void RingBuffer::push(uint8_t item) {
 | 
			
		||||
  this->buffer_[this->insert_pos_] = item;
 | 
			
		||||
  this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
 | 
			
		||||
}
 | 
			
		||||
void RingBuffer::push(const uint8_t *data, size_t len) {
 | 
			
		||||
  for (size_t i = 0; i != len; i++) {
 | 
			
		||||
    this->buffer_[this->insert_pos_] = *data++;
 | 
			
		||||
    this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t RingBuffer::pop() {
 | 
			
		||||
  uint8_t item = this->buffer_[this->read_pos_];
 | 
			
		||||
  this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
 | 
			
		||||
  return item;
 | 
			
		||||
}
 | 
			
		||||
size_t RingBuffer::pop(uint8_t *data, size_t len) {
 | 
			
		||||
  len = std::min(len, this->get_available());
 | 
			
		||||
  for (size_t i = 0; i != len; i++) {
 | 
			
		||||
    *data++ = this->buffer_[this->read_pos_];
 | 
			
		||||
    this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
 | 
			
		||||
  }
 | 
			
		||||
  return len;
 | 
			
		||||
}
 | 
			
		||||
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
 | 
			
		||||
  if (!this->initialised_) {
 | 
			
		||||
    ESP_LOGV(TAG, "Channel not initialised - write ignored");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  while (this->output_buffer_.get_free_space() != 0 && len-- != 0) {
 | 
			
		||||
    this->output_buffer_.push(*data++);
 | 
			
		||||
  }
 | 
			
		||||
  len++;
 | 
			
		||||
  if (len > 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len);
 | 
			
		||||
  }
 | 
			
		||||
  this->parent_->start_output(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool USBUartChannel::peek_byte(uint8_t *data) {
 | 
			
		||||
  if (this->input_buffer_.is_empty()) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  *data = this->input_buffer_.peek();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
 | 
			
		||||
  if (!this->initialised_) {
 | 
			
		||||
    ESP_LOGV(TAG, "Channel not initialised - read ignored");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  auto available = this->available();
 | 
			
		||||
  bool status = true;
 | 
			
		||||
  if (len > available) {
 | 
			
		||||
    ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available);
 | 
			
		||||
    len = available;
 | 
			
		||||
    status = false;
 | 
			
		||||
  }
 | 
			
		||||
  for (size_t i = 0; i != len; i++) {
 | 
			
		||||
    *data++ = this->input_buffer_.pop();
 | 
			
		||||
  }
 | 
			
		||||
  this->parent_->start_input(this);
 | 
			
		||||
  return status;
 | 
			
		||||
}
 | 
			
		||||
void USBUartComponent::setup() { USBClient::setup(); }
 | 
			
		||||
void USBUartComponent::loop() { USBClient::loop(); }
 | 
			
		||||
void USBUartComponent::dump_config() {
 | 
			
		||||
  USBClient::dump_config();
 | 
			
		||||
  for (auto &channel : this->channels_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  UART Channel %d", channel->index_);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Baud Rate: %" PRIu32 " baud", channel->baud_rate_);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Data Bits: %u", channel->data_bits_);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Parity: %s", PARITY_NAMES[channel->parity_]);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Stop bits: %s", STOP_BITS_NAMES[channel->stop_bits_]);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Debug: %s", YESNO(channel->debug_));
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Dummy receiver: %s", YESNO(channel->dummy_receiver_));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBUartComponent::start_input(USBUartChannel *channel) {
 | 
			
		||||
  if (!channel->initialised_ || channel->input_started_ ||
 | 
			
		||||
      channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
 | 
			
		||||
    return;
 | 
			
		||||
  auto ep = channel->cdc_dev_.in_ep;
 | 
			
		||||
  auto callback = [this, channel](const usb_host::TransferStatus &status) {
 | 
			
		||||
    ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
 | 
			
		||||
    if (!status.success) {
 | 
			
		||||
      ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
#ifdef USE_UART_DEBUGGER
 | 
			
		||||
    if (channel->debug_) {
 | 
			
		||||
      uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
 | 
			
		||||
                               std::vector<uint8_t>(status.data, status.data + status.data_len), ',');  // NOLINT()
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    channel->input_started_ = false;
 | 
			
		||||
    if (!channel->dummy_receiver_) {
 | 
			
		||||
      for (size_t i = 0; i != status.data_len; i++) {
 | 
			
		||||
        channel->input_buffer_.push(status.data[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
 | 
			
		||||
      this->defer([this, channel] { this->start_input(channel); });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  channel->input_started_ = true;
 | 
			
		||||
  this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartComponent::start_output(USBUartChannel *channel) {
 | 
			
		||||
  if (channel->output_started_)
 | 
			
		||||
    return;
 | 
			
		||||
  if (channel->output_buffer_.is_empty()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  auto ep = channel->cdc_dev_.out_ep;
 | 
			
		||||
  auto callback = [this, channel](const usb_host::TransferStatus &status) {
 | 
			
		||||
    ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
 | 
			
		||||
    channel->output_started_ = false;
 | 
			
		||||
    this->defer([this, channel] { this->start_output(channel); });
 | 
			
		||||
  };
 | 
			
		||||
  channel->output_started_ = true;
 | 
			
		||||
  uint8_t data[ep->wMaxPacketSize];
 | 
			
		||||
  auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
 | 
			
		||||
  this->transfer_out(ep->bEndpointAddress, callback, data, len);
 | 
			
		||||
#ifdef USE_UART_DEBUGGER
 | 
			
		||||
  if (channel->debug_) {
 | 
			
		||||
    uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector<uint8_t>(data, data + len), ',');  // NOLINT()
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  ESP_LOGV(TAG, "Output %d bytes started", len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Hacky fix for some devices that report incorrect MPS values
 | 
			
		||||
 * @param ep The endpoint descriptor
 | 
			
		||||
 */
 | 
			
		||||
static void fix_mps(const usb_ep_desc_t *ep) {
 | 
			
		||||
  if (ep != nullptr) {
 | 
			
		||||
    auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep);
 | 
			
		||||
    if (ep->wMaxPacketSize > 64) {
 | 
			
		||||
      ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize);
 | 
			
		||||
      ep_mutable->wMaxPacketSize = 64;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void USBUartTypeCdcAcm::on_connected() {
 | 
			
		||||
  auto cdc_devs = this->parse_descriptors_(this->device_handle_);
 | 
			
		||||
  if (cdc_devs.empty()) {
 | 
			
		||||
    this->status_set_error("No CDC-ACM device found");
 | 
			
		||||
    this->disconnect();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size());
 | 
			
		||||
  auto i = 0;
 | 
			
		||||
  for (auto channel : this->channels_) {
 | 
			
		||||
    if (i == cdc_devs.size()) {
 | 
			
		||||
      ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
 | 
			
		||||
      this->status_set_warning("No configuration found for channel");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    channel->cdc_dev_ = cdc_devs[i++];
 | 
			
		||||
    fix_mps(channel->cdc_dev_.in_ep);
 | 
			
		||||
    fix_mps(channel->cdc_dev_.out_ep);
 | 
			
		||||
    channel->initialised_ = true;
 | 
			
		||||
    auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number, 0);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
 | 
			
		||||
               channel->cdc_dev_.interface_number);
 | 
			
		||||
      this->status_set_error("usb_host_interface_claim failed");
 | 
			
		||||
      this->disconnect();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->enable_channels();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartTypeCdcAcm::on_disconnected() {
 | 
			
		||||
  for (auto channel : this->channels_) {
 | 
			
		||||
    if (channel->cdc_dev_.in_ep != nullptr) {
 | 
			
		||||
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
 | 
			
		||||
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
 | 
			
		||||
    }
 | 
			
		||||
    if (channel->cdc_dev_.out_ep != nullptr) {
 | 
			
		||||
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
 | 
			
		||||
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
 | 
			
		||||
    }
 | 
			
		||||
    if (channel->cdc_dev_.notify_ep != nullptr) {
 | 
			
		||||
      usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
 | 
			
		||||
      usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
 | 
			
		||||
    }
 | 
			
		||||
    usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number);
 | 
			
		||||
    channel->initialised_ = false;
 | 
			
		||||
    channel->input_started_ = false;
 | 
			
		||||
    channel->output_started_ = false;
 | 
			
		||||
    channel->input_buffer_.clear();
 | 
			
		||||
    channel->output_buffer_.clear();
 | 
			
		||||
  }
 | 
			
		||||
  USBClient::on_disconnected();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void USBUartTypeCdcAcm::enable_channels() {
 | 
			
		||||
  for (auto channel : this->channels_) {
 | 
			
		||||
    if (!channel->initialised_)
 | 
			
		||||
      continue;
 | 
			
		||||
    channel->input_started_ = false;
 | 
			
		||||
    channel->output_started_ = false;
 | 
			
		||||
    this->start_input(channel);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_uart
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
							
								
								
									
										151
									
								
								esphome/components/usb_uart/usb_uart.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								esphome/components/usb_uart/usb_uart.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/uart/uart_component.h"
 | 
			
		||||
#include "esphome/components/usb_host/usb_host.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace usb_uart {
 | 
			
		||||
class USBUartTypeCdcAcm;
 | 
			
		||||
class USBUartComponent;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "usb_uart";
 | 
			
		||||
 | 
			
		||||
static constexpr uint8_t USB_CDC_SUBCLASS_ACM = 0x02;
 | 
			
		||||
static constexpr uint8_t USB_SUBCLASS_COMMON = 0x02;
 | 
			
		||||
static constexpr uint8_t USB_SUBCLASS_NULL = 0x00;
 | 
			
		||||
static constexpr uint8_t USB_PROTOCOL_NULL = 0x00;
 | 
			
		||||
static constexpr uint8_t USB_DEVICE_PROTOCOL_IAD = 0x01;
 | 
			
		||||
static constexpr uint8_t USB_VENDOR_IFC = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_INTERFACE;
 | 
			
		||||
static constexpr uint8_t USB_VENDOR_DEV = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_DEVICE;
 | 
			
		||||
 | 
			
		||||
struct CdcEps {
 | 
			
		||||
  const usb_ep_desc_t *notify_ep;
 | 
			
		||||
  const usb_ep_desc_t *in_ep;
 | 
			
		||||
  const usb_ep_desc_t *out_ep;
 | 
			
		||||
  uint8_t interface_number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum UARTParityOptions {
 | 
			
		||||
  UART_CONFIG_PARITY_NONE = 0,
 | 
			
		||||
  UART_CONFIG_PARITY_ODD,
 | 
			
		||||
  UART_CONFIG_PARITY_EVEN,
 | 
			
		||||
  UART_CONFIG_PARITY_MARK,
 | 
			
		||||
  UART_CONFIG_PARITY_SPACE,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum UARTStopBitsOptions {
 | 
			
		||||
  UART_CONFIG_STOP_BITS_1 = 0,
 | 
			
		||||
  UART_CONFIG_STOP_BITS_1_5,
 | 
			
		||||
  UART_CONFIG_STOP_BITS_2,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static const char *const PARITY_NAMES[] = {"NONE", "ODD", "EVEN", "MARK", "SPACE"};
 | 
			
		||||
static const char *const STOP_BITS_NAMES[] = {"1", "1.5", "2"};
 | 
			
		||||
 | 
			
		||||
class RingBuffer {
 | 
			
		||||
 public:
 | 
			
		||||
  RingBuffer(uint16_t buffer_size) : buffer_size_(buffer_size), buffer_(new uint8_t[buffer_size]) {}
 | 
			
		||||
  bool is_empty() const { return this->read_pos_ == this->insert_pos_; }
 | 
			
		||||
  size_t get_available() const {
 | 
			
		||||
    return (this->insert_pos_ + this->buffer_size_ - this->read_pos_) % this->buffer_size_;
 | 
			
		||||
  };
 | 
			
		||||
  size_t get_free_space() const { return this->buffer_size_ - 1 - this->get_available(); }
 | 
			
		||||
  uint8_t peek() const { return this->buffer_[this->read_pos_]; }
 | 
			
		||||
  void push(uint8_t item);
 | 
			
		||||
  void push(const uint8_t *data, size_t len);
 | 
			
		||||
  uint8_t pop();
 | 
			
		||||
  size_t pop(uint8_t *data, size_t len);
 | 
			
		||||
  void clear() { this->read_pos_ = this->insert_pos_ = 0; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint16_t insert_pos_ = 0;
 | 
			
		||||
  uint16_t read_pos_ = 0;
 | 
			
		||||
  uint16_t buffer_size_;
 | 
			
		||||
  uint8_t *buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
 | 
			
		||||
  friend class USBUartComponent;
 | 
			
		||||
  friend class USBUartTypeCdcAcm;
 | 
			
		||||
  friend class USBUartTypeCP210X;
 | 
			
		||||
  friend class USBUartTypeCH34X;
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  USBUartChannel(uint8_t index, uint16_t buffer_size)
 | 
			
		||||
      : index_(index), input_buffer_(RingBuffer(buffer_size)), output_buffer_(RingBuffer(buffer_size)) {}
 | 
			
		||||
  void write_array(const uint8_t *data, size_t len) override;
 | 
			
		||||
  ;
 | 
			
		||||
  bool peek_byte(uint8_t *data) override;
 | 
			
		||||
  ;
 | 
			
		||||
  bool read_array(uint8_t *data, size_t len) override;
 | 
			
		||||
  int available() override { return static_cast<int>(this->input_buffer_.get_available()); }
 | 
			
		||||
  void flush() override {}
 | 
			
		||||
  void check_logger_conflict() override {}
 | 
			
		||||
  void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
 | 
			
		||||
  void set_debug(bool debug) { this->debug_ = debug; }
 | 
			
		||||
  void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  const uint8_t index_;
 | 
			
		||||
  RingBuffer input_buffer_;
 | 
			
		||||
  RingBuffer output_buffer_;
 | 
			
		||||
  UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
 | 
			
		||||
  bool input_started_{true};
 | 
			
		||||
  bool output_started_{true};
 | 
			
		||||
  CdcEps cdc_dev_{};
 | 
			
		||||
  bool debug_{};
 | 
			
		||||
  bool dummy_receiver_{};
 | 
			
		||||
  bool initialised_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class USBUartComponent : public usb_host::USBClient {
 | 
			
		||||
 public:
 | 
			
		||||
  USBUartComponent(uint16_t vid, uint16_t pid) : usb_host::USBClient(vid, pid) {}
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  std::vector<USBUartChannel *> get_channels() { return this->channels_; }
 | 
			
		||||
 | 
			
		||||
  void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
 | 
			
		||||
 | 
			
		||||
  void start_input(USBUartChannel *channel);
 | 
			
		||||
  void start_output(USBUartChannel *channel);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<USBUartChannel *> channels_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class USBUartTypeCdcAcm : public USBUartComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  USBUartTypeCdcAcm(uint16_t vid, uint16_t pid) : USBUartComponent(vid, pid) {}
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual std::vector<CdcEps> parse_descriptors_(usb_device_handle_t dev_hdl);
 | 
			
		||||
  void on_connected() override;
 | 
			
		||||
  virtual void enable_channels();
 | 
			
		||||
  void on_disconnected() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class USBUartTypeCP210X : public USBUartTypeCdcAcm {
 | 
			
		||||
 public:
 | 
			
		||||
  USBUartTypeCP210X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<CdcEps> parse_descriptors_(usb_device_handle_t dev_hdl) override;
 | 
			
		||||
  void enable_channels() override;
 | 
			
		||||
};
 | 
			
		||||
class USBUartTypeCH34X : public USBUartTypeCdcAcm {
 | 
			
		||||
 public:
 | 
			
		||||
  USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void enable_channels() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_uart
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3
 | 
			
		||||
@@ -483,14 +483,16 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
 | 
			
		||||
    // Enable WiFi
 | 
			
		||||
    global_wifi_component->enable();
 | 
			
		||||
    // Set timeout for the connection
 | 
			
		||||
    this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this]() {
 | 
			
		||||
      this->connecting_ = false;
 | 
			
		||||
    this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() {
 | 
			
		||||
      // If the timeout is reached, stop connecting and revert to the old AP
 | 
			
		||||
      global_wifi_component->disable();
 | 
			
		||||
      global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
 | 
			
		||||
      global_wifi_component->enable();
 | 
			
		||||
      // Callback to notify the user that the connection failed
 | 
			
		||||
      this->error_trigger_->trigger();
 | 
			
		||||
      // Start a timeout for the fallback if the connection to the old AP fails
 | 
			
		||||
      this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
 | 
			
		||||
        this->connecting_ = false;
 | 
			
		||||
        this->error_trigger_->trigger();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -503,6 +505,7 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
 | 
			
		||||
    if (global_wifi_component->is_connected()) {
 | 
			
		||||
      // The WiFi is connected, stop the timeout and reset the connecting flag
 | 
			
		||||
      this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
      this->cancel_timeout("wifi-fallback-timeout");
 | 
			
		||||
      this->connecting_ = false;
 | 
			
		||||
      if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
 | 
			
		||||
        // Callback to notify the user that the connection was successful
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,10 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import text_sensor, uart
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import ICON_FINGERPRINT
 | 
			
		||||
from esphome.const import CONF_RESET, ICON_FINGERPRINT
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@hobbypunk90"]
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
CONF_RESET = "reset"
 | 
			
		||||
 | 
			
		||||
wl134_ns = cg.esphome_ns.namespace("wl_134")
 | 
			
		||||
Wl134Component = wl134_ns.class_(
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ import functools
 | 
			
		||||
import heapq
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
import voluptuous as vol
 | 
			
		||||
 | 
			
		||||
@@ -63,7 +63,7 @@ def iter_component_configs(config):
 | 
			
		||||
                yield p_name, platform, p_config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ConfigPath = list[Union[str, int]]
 | 
			
		||||
ConfigPath = list[str | int]
 | 
			
		||||
path_context = contextvars.ContextVar("Config path")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -982,23 +982,32 @@ def uuid(value):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
METRIC_SUFFIXES = {
 | 
			
		||||
    "E": 1e18,
 | 
			
		||||
    "P": 1e15,
 | 
			
		||||
    "T": 1e12,
 | 
			
		||||
    "G": 1e9,
 | 
			
		||||
    "M": 1e6,
 | 
			
		||||
    "k": 1e3,
 | 
			
		||||
    "da": 10,
 | 
			
		||||
    "d": 1e-1,
 | 
			
		||||
    "c": 1e-2,
 | 
			
		||||
    "m": 0.001,
 | 
			
		||||
    "µ": 1e-6,
 | 
			
		||||
    "u": 1e-6,
 | 
			
		||||
    "n": 1e-9,
 | 
			
		||||
    "p": 1e-12,
 | 
			
		||||
    "f": 1e-15,
 | 
			
		||||
    "a": 1e-18,
 | 
			
		||||
    "": 1,
 | 
			
		||||
    "Q": 1e30,  # Quetta
 | 
			
		||||
    "R": 1e27,  # Ronna
 | 
			
		||||
    "Y": 1e24,  # Yotta
 | 
			
		||||
    "Z": 1e21,  # Zetta
 | 
			
		||||
    "E": 1e18,  # Exa
 | 
			
		||||
    "P": 1e15,  # Peta
 | 
			
		||||
    "T": 1e12,  # Tera
 | 
			
		||||
    "G": 1e9,  # Giga
 | 
			
		||||
    "M": 1e6,  # Mega
 | 
			
		||||
    "k": 1e3,  # Kilo
 | 
			
		||||
    "h": 1e2,  # Hecto
 | 
			
		||||
    "da": 1e1,  # Deca
 | 
			
		||||
    "": 1e0,  # No prefix
 | 
			
		||||
    "d": 1e-1,  # Deci
 | 
			
		||||
    "c": 1e-2,  # Centi
 | 
			
		||||
    "m": 1e-3,  # Milli
 | 
			
		||||
    "µ": 1e-6,  # Micro
 | 
			
		||||
    "u": 1e-6,  # Micro (same as µ)
 | 
			
		||||
    "n": 1e-9,  # Nano
 | 
			
		||||
    "p": 1e-12,  # Pico
 | 
			
		||||
    "f": 1e-15,  # Femto
 | 
			
		||||
    "a": 1e-18,  # Atto
 | 
			
		||||
    "z": 1e-21,  # Zepto
 | 
			
		||||
    "y": 1e-24,  # Yocto
 | 
			
		||||
    "r": 1e-27,  # Ronto
 | 
			
		||||
    "q": 1e-30,  # Quecto
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
"""Constants used by esphome."""
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.5.0b4"
 | 
			
		||||
__version__ = "2025.6.0-dev"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
@@ -735,6 +735,7 @@ CONF_REFRESH = "refresh"
 | 
			
		||||
CONF_RELABEL = "relabel"
 | 
			
		||||
CONF_REPEAT = "repeat"
 | 
			
		||||
CONF_REPOSITORY = "repository"
 | 
			
		||||
CONF_RESET = "reset"
 | 
			
		||||
CONF_RESET_DURATION = "reset_duration"
 | 
			
		||||
CONF_RESET_PIN = "reset_pin"
 | 
			
		||||
CONF_RESIZE = "resize"
 | 
			
		||||
@@ -1130,11 +1131,13 @@ UNIT_WATT_HOURS = "Wh"
 | 
			
		||||
# device classes
 | 
			
		||||
DEVICE_CLASS_APPARENT_POWER = "apparent_power"
 | 
			
		||||
DEVICE_CLASS_AQI = "aqi"
 | 
			
		||||
DEVICE_CLASS_AREA = "area"
 | 
			
		||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
 | 
			
		||||
DEVICE_CLASS_AWNING = "awning"
 | 
			
		||||
DEVICE_CLASS_BATTERY = "battery"
 | 
			
		||||
DEVICE_CLASS_BATTERY_CHARGING = "battery_charging"
 | 
			
		||||
DEVICE_CLASS_BLIND = "blind"
 | 
			
		||||
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
 | 
			
		||||
DEVICE_CLASS_BUTTON = "button"
 | 
			
		||||
DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
 | 
			
		||||
DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
 | 
			
		||||
@@ -1153,6 +1156,7 @@ DEVICE_CLASS_DOORBELL = "doorbell"
 | 
			
		||||
DEVICE_CLASS_DURATION = "duration"
 | 
			
		||||
DEVICE_CLASS_EMPTY = ""
 | 
			
		||||
DEVICE_CLASS_ENERGY = "energy"
 | 
			
		||||
DEVICE_CLASS_ENERGY_DISTANCE = "energy_distance"
 | 
			
		||||
DEVICE_CLASS_ENERGY_STORAGE = "energy_storage"
 | 
			
		||||
DEVICE_CLASS_FIRMWARE = "firmware"
 | 
			
		||||
DEVICE_CLASS_FREQUENCY = "frequency"
 | 
			
		||||
@@ -1190,6 +1194,7 @@ DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity"
 | 
			
		||||
DEVICE_CLASS_PRESENCE = "presence"
 | 
			
		||||
DEVICE_CLASS_PRESSURE = "pressure"
 | 
			
		||||
DEVICE_CLASS_PROBLEM = "problem"
 | 
			
		||||
DEVICE_CLASS_REACTIVE_ENERGY = "reactive_energy"
 | 
			
		||||
DEVICE_CLASS_REACTIVE_POWER = "reactive_power"
 | 
			
		||||
DEVICE_CLASS_RESTART = "restart"
 | 
			
		||||
DEVICE_CLASS_RUNNING = "running"
 | 
			
		||||
@@ -1217,6 +1222,7 @@ DEVICE_CLASS_VOLUME_STORAGE = "volume_storage"
 | 
			
		||||
DEVICE_CLASS_WATER = "water"
 | 
			
		||||
DEVICE_CLASS_WEIGHT = "weight"
 | 
			
		||||
DEVICE_CLASS_WINDOW = "window"
 | 
			
		||||
DEVICE_CLASS_WIND_DIRECTION = "wind_direction"
 | 
			
		||||
DEVICE_CLASS_WIND_SPEED = "wind_speed"
 | 
			
		||||
 | 
			
		||||
# state classes
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import logging
 | 
			
		||||
import math
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
from typing import TYPE_CHECKING, Optional, Union
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_COMMENT,
 | 
			
		||||
@@ -326,7 +326,7 @@ class ID:
 | 
			
		||||
        else:
 | 
			
		||||
            self.is_manual = is_manual
 | 
			
		||||
        self.is_declaration = is_declaration
 | 
			
		||||
        self.type: Optional[MockObjClass] = type
 | 
			
		||||
        self.type: MockObjClass | None = type
 | 
			
		||||
 | 
			
		||||
    def resolve(self, registered_ids):
 | 
			
		||||
        from esphome.config_validation import RESERVED_IDS
 | 
			
		||||
@@ -476,20 +476,20 @@ class EsphomeCore:
 | 
			
		||||
        # True if command is run from vscode api
 | 
			
		||||
        self.vscode = False
 | 
			
		||||
        # The name of the node
 | 
			
		||||
        self.name: Optional[str] = None
 | 
			
		||||
        self.name: str | None = None
 | 
			
		||||
        # The friendly name of the node
 | 
			
		||||
        self.friendly_name: Optional[str] = None
 | 
			
		||||
        self.friendly_name: str | None = None
 | 
			
		||||
        # The area / zone of the node
 | 
			
		||||
        self.area: Optional[str] = None
 | 
			
		||||
        self.area: str | None = None
 | 
			
		||||
        # Additional data components can store temporary data in
 | 
			
		||||
        # The first key to this dict should always be the integration name
 | 
			
		||||
        self.data = {}
 | 
			
		||||
        # The relative path to the configuration YAML
 | 
			
		||||
        self.config_path: Optional[str] = None
 | 
			
		||||
        self.config_path: str | None = None
 | 
			
		||||
        # The relative path to where all build files are stored
 | 
			
		||||
        self.build_path: Optional[str] = None
 | 
			
		||||
        self.build_path: str | None = None
 | 
			
		||||
        # The validated configuration, this is None until the config has been validated
 | 
			
		||||
        self.config: Optional[ConfigType] = None
 | 
			
		||||
        self.config: ConfigType | None = None
 | 
			
		||||
        # The pending tasks in the task queue (mostly for C++ generation)
 | 
			
		||||
        # This is a priority queue (with heapq)
 | 
			
		||||
        # Each item is a tuple of form: (-priority, unique number, task)
 | 
			
		||||
@@ -509,7 +509,7 @@ class EsphomeCore:
 | 
			
		||||
        # A set of defines to set for the compile process in esphome/core/defines.h
 | 
			
		||||
        self.defines: set[Define] = set()
 | 
			
		||||
        # A map of all platformio options to apply
 | 
			
		||||
        self.platformio_options: dict[str, Union[str, list[str]]] = {}
 | 
			
		||||
        self.platformio_options: dict[str, str | list[str]] = {}
 | 
			
		||||
        # A set of strings of names of loaded integrations, used to find namespace ID conflicts
 | 
			
		||||
        self.loaded_integrations = set()
 | 
			
		||||
        # A set of component IDs to track what Component subclasses are declared
 | 
			
		||||
@@ -546,7 +546,7 @@ class EsphomeCore:
 | 
			
		||||
        PIN_SCHEMA_REGISTRY.reset()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def address(self) -> Optional[str]:
 | 
			
		||||
    def address(self) -> str | None:
 | 
			
		||||
        if self.config is None:
 | 
			
		||||
            raise ValueError("Config has not been loaded yet")
 | 
			
		||||
 | 
			
		||||
@@ -559,7 +559,7 @@ class EsphomeCore:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def web_port(self) -> Optional[int]:
 | 
			
		||||
    def web_port(self) -> int | None:
 | 
			
		||||
        if self.config is None:
 | 
			
		||||
            raise ValueError("Config has not been loaded yet")
 | 
			
		||||
 | 
			
		||||
@@ -572,7 +572,7 @@ class EsphomeCore:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def comment(self) -> Optional[str]:
 | 
			
		||||
    def comment(self) -> str | None:
 | 
			
		||||
        if self.config is None:
 | 
			
		||||
            raise ValueError("Config has not been loaded yet")
 | 
			
		||||
 | 
			
		||||
@@ -773,7 +773,7 @@ class EsphomeCore:
 | 
			
		||||
        _LOGGER.debug("Adding define: %s", define)
 | 
			
		||||
        return define
 | 
			
		||||
 | 
			
		||||
    def add_platformio_option(self, key: str, value: Union[str, list[str]]) -> None:
 | 
			
		||||
    def add_platformio_option(self, key: str, value: str | list[str]) -> None:
 | 
			
		||||
        new_val = value
 | 
			
		||||
        old_val = self.platformio_options.get(key)
 | 
			
		||||
        if isinstance(old_val, list):
 | 
			
		||||
 
 | 
			
		||||
@@ -160,7 +160,8 @@
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2)
 | 
			
		||||
#define USE_LOGGER_USB_CDC
 | 
			
		||||
#elif defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \
 | 
			
		||||
    defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
 | 
			
		||||
    defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \
 | 
			
		||||
    defined(USE_ESP32_VARIANT_ESP32P4)
 | 
			
		||||
#define USE_LOGGER_USB_CDC
 | 
			
		||||
#define USE_LOGGER_USB_SERIAL_JTAG
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <limits>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <iterator>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 
 | 
			
		||||
@@ -42,14 +42,13 @@ Here everything is combined in `yield` expressions. You await other coroutines u
 | 
			
		||||
the last `yield` expression defines what is returned.
 | 
			
		||||
"""
 | 
			
		||||
 | 
			
		||||
import collections
 | 
			
		||||
from collections.abc import Awaitable, Generator, Iterator
 | 
			
		||||
from collections.abc import Awaitable, Callable, Generator, Iterator
 | 
			
		||||
import functools
 | 
			
		||||
import heapq
 | 
			
		||||
import inspect
 | 
			
		||||
import logging
 | 
			
		||||
import types
 | 
			
		||||
from typing import Any, Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
@@ -126,7 +125,7 @@ def _flatten_generator(gen: Generator[Any, Any, Any]):
 | 
			
		||||
            ret = to_send if e.value is None else e.value
 | 
			
		||||
            return ret
 | 
			
		||||
 | 
			
		||||
        if isinstance(val, collections.abc.Awaitable):
 | 
			
		||||
        if isinstance(val, Awaitable):
 | 
			
		||||
            # yielded object that is awaitable (like `yield some_new_style_method()`)
 | 
			
		||||
            # yield from __await__() like actual coroutines would.
 | 
			
		||||
            to_send = yield from val.__await__()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,9 @@
 | 
			
		||||
import abc
 | 
			
		||||
from collections.abc import Sequence
 | 
			
		||||
from collections.abc import Callable, Sequence
 | 
			
		||||
import inspect
 | 
			
		||||
import math
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any, Callable, Optional, Union
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome.core import (
 | 
			
		||||
    CORE,
 | 
			
		||||
@@ -35,19 +35,19 @@ class Expression(abc.ABC):
 | 
			
		||||
        """
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
SafeExpType = Union[
 | 
			
		||||
    Expression,
 | 
			
		||||
    bool,
 | 
			
		||||
    str,
 | 
			
		||||
    str,
 | 
			
		||||
    int,
 | 
			
		||||
    float,
 | 
			
		||||
    TimePeriod,
 | 
			
		||||
    type[bool],
 | 
			
		||||
    type[int],
 | 
			
		||||
    type[float],
 | 
			
		||||
    Sequence[Any],
 | 
			
		||||
]
 | 
			
		||||
SafeExpType = (
 | 
			
		||||
    Expression
 | 
			
		||||
    | bool
 | 
			
		||||
    | str
 | 
			
		||||
    | str
 | 
			
		||||
    | int
 | 
			
		||||
    | float
 | 
			
		||||
    | TimePeriod
 | 
			
		||||
    | type[bool]
 | 
			
		||||
    | type[int]
 | 
			
		||||
    | type[float]
 | 
			
		||||
    | Sequence[Any]
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RawExpression(Expression):
 | 
			
		||||
@@ -90,7 +90,7 @@ class VariableDeclarationExpression(Expression):
 | 
			
		||||
class ExpressionList(Expression):
 | 
			
		||||
    __slots__ = ("args",)
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args: Optional[SafeExpType]):
 | 
			
		||||
    def __init__(self, *args: SafeExpType | None):
 | 
			
		||||
        # Remove every None on end
 | 
			
		||||
        args = list(args)
 | 
			
		||||
        while args and args[-1] is None:
 | 
			
		||||
@@ -139,7 +139,7 @@ class CallExpression(Expression):
 | 
			
		||||
class StructInitializer(Expression):
 | 
			
		||||
    __slots__ = ("base", "args")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, base: Expression, *args: tuple[str, Optional[SafeExpType]]):
 | 
			
		||||
    def __init__(self, base: Expression, *args: tuple[str, SafeExpType | None]):
 | 
			
		||||
        self.base = base
 | 
			
		||||
        # TODO: args is always a Tuple, is this check required?
 | 
			
		||||
        if not isinstance(args, OrderedDict):
 | 
			
		||||
@@ -197,9 +197,7 @@ class ParameterExpression(Expression):
 | 
			
		||||
class ParameterListExpression(Expression):
 | 
			
		||||
    __slots__ = ("parameters",)
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]]
 | 
			
		||||
    ):
 | 
			
		||||
    def __init__(self, *parameters: ParameterExpression | tuple[SafeExpType, str]):
 | 
			
		||||
        self.parameters = []
 | 
			
		||||
        for parameter in parameters:
 | 
			
		||||
            if not isinstance(parameter, ParameterExpression):
 | 
			
		||||
@@ -362,7 +360,7 @@ def safe_exp(obj: SafeExpType) -> Expression:
 | 
			
		||||
        return IntLiteral(int(obj.total_seconds))
 | 
			
		||||
    if isinstance(obj, TimePeriodMinutes):
 | 
			
		||||
        return IntLiteral(int(obj.total_minutes))
 | 
			
		||||
    if isinstance(obj, (tuple, list)):
 | 
			
		||||
    if isinstance(obj, tuple | list):
 | 
			
		||||
        return ArrayInitializer(*[safe_exp(o) for o in obj])
 | 
			
		||||
    if obj is bool:
 | 
			
		||||
        return bool_
 | 
			
		||||
@@ -418,7 +416,9 @@ class LineComment(Statement):
 | 
			
		||||
        self.value = value
 | 
			
		||||
 | 
			
		||||
    def __str__(self):
 | 
			
		||||
        parts = re.sub(r"\\\s*\n", r"<cont>\n", self.value, re.MULTILINE).split("\n")
 | 
			
		||||
        parts = re.sub(r"\\\s*\n", r"<cont>\n", self.value, flags=re.MULTILINE).split(
 | 
			
		||||
            "\n"
 | 
			
		||||
        )
 | 
			
		||||
        parts = [f"// {x}" for x in parts]
 | 
			
		||||
        return "\n".join(parts)
 | 
			
		||||
 | 
			
		||||
@@ -461,7 +461,7 @@ def static_const_array(id_, rhs) -> "MockObj":
 | 
			
		||||
    return obj
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def statement(expression: Union[Expression, Statement]) -> Statement:
 | 
			
		||||
def statement(expression: Expression | Statement) -> Statement:
 | 
			
		||||
    """Convert expression into a statement unless is already a statement."""
 | 
			
		||||
    if isinstance(expression, Statement):
 | 
			
		||||
        return expression
 | 
			
		||||
@@ -579,7 +579,7 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
 | 
			
		||||
    return Pvariable(id_, rhs)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add(expression: Union[Expression, Statement]):
 | 
			
		||||
def add(expression: Expression | Statement):
 | 
			
		||||
    """Add an expression to the codegen section.
 | 
			
		||||
 | 
			
		||||
    After this is called, the given given expression will
 | 
			
		||||
@@ -588,12 +588,12 @@ def add(expression: Union[Expression, Statement]):
 | 
			
		||||
    CORE.add(expression)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_global(expression: Union[SafeExpType, Statement], prepend: bool = False):
 | 
			
		||||
def add_global(expression: SafeExpType | Statement, prepend: bool = False):
 | 
			
		||||
    """Add an expression to the codegen global storage (above setup())."""
 | 
			
		||||
    CORE.add_global(expression, prepend)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_library(name: str, version: Optional[str], repository: Optional[str] = None):
 | 
			
		||||
def add_library(name: str, version: str | None, repository: str | None = None):
 | 
			
		||||
    """Add a library to the codegen library storage.
 | 
			
		||||
 | 
			
		||||
    :param name: The name of the library (for example 'AsyncTCP')
 | 
			
		||||
@@ -619,7 +619,7 @@ def add_define(name: str, value: SafeExpType = None):
 | 
			
		||||
        CORE.add_define(Define(name, safe_exp(value)))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def add_platformio_option(key: str, value: Union[str, list[str]]):
 | 
			
		||||
def add_platformio_option(key: str, value: str | list[str]):
 | 
			
		||||
    CORE.add_platformio_option(key, value)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -654,7 +654,7 @@ async def process_lambda(
 | 
			
		||||
    parameters: list[tuple[SafeExpType, str]],
 | 
			
		||||
    capture: str = "=",
 | 
			
		||||
    return_type: SafeExpType = None,
 | 
			
		||||
) -> Union[LambdaExpression, None]:
 | 
			
		||||
) -> LambdaExpression | None:
 | 
			
		||||
    """Process the given lambda value into a LambdaExpression.
 | 
			
		||||
 | 
			
		||||
    This is a coroutine because lambdas can depend on other IDs,
 | 
			
		||||
@@ -711,8 +711,8 @@ def is_template(value):
 | 
			
		||||
async def templatable(
 | 
			
		||||
    value: Any,
 | 
			
		||||
    args: list[tuple[SafeExpType, str]],
 | 
			
		||||
    output_type: Optional[SafeExpType],
 | 
			
		||||
    to_exp: Union[Callable, dict] = None,
 | 
			
		||||
    output_type: SafeExpType | None,
 | 
			
		||||
    to_exp: Callable | dict = None,
 | 
			
		||||
):
 | 
			
		||||
    """Generate code for a templatable config option.
 | 
			
		||||
 | 
			
		||||
@@ -821,7 +821,7 @@ class MockObj(Expression):
 | 
			
		||||
        assert self.op == "::"
 | 
			
		||||
        return MockObj(f"using namespace {self.base}")
 | 
			
		||||
 | 
			
		||||
    def __getitem__(self, item: Union[str, Expression]) -> "MockObj":
 | 
			
		||||
    def __getitem__(self, item: str | Expression) -> "MockObj":
 | 
			
		||||
        next_op = "."
 | 
			
		||||
        if isinstance(item, str) and item.startswith("P"):
 | 
			
		||||
            item = item[1:]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import asyncio
 | 
			
		||||
from collections.abc import Coroutine
 | 
			
		||||
from collections.abc import Callable, Coroutine
 | 
			
		||||
import contextlib
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from functools import partial
 | 
			
		||||
@@ -9,7 +9,7 @@ import json
 | 
			
		||||
import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import threading
 | 
			
		||||
from typing import Any, Callable
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome.storage_json import ignored_devices_storage_path
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
import asyncio
 | 
			
		||||
import base64
 | 
			
		||||
from collections.abc import Iterable
 | 
			
		||||
from collections.abc import Callable, Iterable
 | 
			
		||||
import datetime
 | 
			
		||||
import functools
 | 
			
		||||
import gzip
 | 
			
		||||
@@ -17,7 +17,7 @@ import shutil
 | 
			
		||||
import subprocess
 | 
			
		||||
import threading
 | 
			
		||||
import time
 | 
			
		||||
from typing import TYPE_CHECKING, Any, Callable, TypeVar
 | 
			
		||||
from typing import TYPE_CHECKING, Any, TypeVar
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
import tornado
 | 
			
		||||
@@ -601,10 +601,12 @@ class DownloadListRequestHandler(BaseHandler):
 | 
			
		||||
        loop = asyncio.get_running_loop()
 | 
			
		||||
        try:
 | 
			
		||||
            downloads_json = await loop.run_in_executor(None, self._get, configuration)
 | 
			
		||||
        except vol.Invalid:
 | 
			
		||||
        except vol.Invalid as exc:
 | 
			
		||||
            _LOGGER.exception("Error while fetching downloads", exc_info=exc)
 | 
			
		||||
            self.send_error(404)
 | 
			
		||||
            return
 | 
			
		||||
        if downloads_json is None:
 | 
			
		||||
            _LOGGER.error("Configuration %s not found", configuration)
 | 
			
		||||
            self.send_error(404)
 | 
			
		||||
            return
 | 
			
		||||
        self.set_status(200)
 | 
			
		||||
@@ -618,14 +620,17 @@ class DownloadListRequestHandler(BaseHandler):
 | 
			
		||||
        if storage_json is None:
 | 
			
		||||
            return None
 | 
			
		||||
 | 
			
		||||
        config = yaml_util.load_yaml(settings.rel_path(configuration))
 | 
			
		||||
        try:
 | 
			
		||||
            config = yaml_util.load_yaml(settings.rel_path(configuration))
 | 
			
		||||
 | 
			
		||||
        if const.CONF_EXTERNAL_COMPONENTS in config:
 | 
			
		||||
            from esphome.components.external_components import (
 | 
			
		||||
                do_external_components_pass,
 | 
			
		||||
            )
 | 
			
		||||
            if const.CONF_EXTERNAL_COMPONENTS in config:
 | 
			
		||||
                from esphome.components.external_components import (
 | 
			
		||||
                    do_external_components_pass,
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            do_external_components_pass(config)
 | 
			
		||||
                do_external_components_pass(config)
 | 
			
		||||
        except vol.Invalid:
 | 
			
		||||
            _LOGGER.info("Could not parse `external_components`, skipping")
 | 
			
		||||
 | 
			
		||||
        from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
from collections.abc import Callable
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
from datetime import datetime
 | 
			
		||||
import hashlib
 | 
			
		||||
@@ -5,7 +6,6 @@ import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
from typing import Callable, Optional
 | 
			
		||||
import urllib.parse
 | 
			
		||||
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
@@ -45,12 +45,12 @@ def clone_or_update(
 | 
			
		||||
    *,
 | 
			
		||||
    url: str,
 | 
			
		||||
    ref: str = None,
 | 
			
		||||
    refresh: Optional[TimePeriodSeconds],
 | 
			
		||||
    refresh: TimePeriodSeconds | None,
 | 
			
		||||
    domain: str,
 | 
			
		||||
    username: str = None,
 | 
			
		||||
    password: str = None,
 | 
			
		||||
    submodules: Optional[list[str]] = None,
 | 
			
		||||
) -> tuple[Path, Optional[Callable[[], None]]]:
 | 
			
		||||
    submodules: list[str] | None = None,
 | 
			
		||||
) -> tuple[Path, Callable[[], None] | None]:
 | 
			
		||||
    key = f"{url}@{ref}"
 | 
			
		||||
 | 
			
		||||
    if username is not None and password is not None:
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ from pathlib import Path
 | 
			
		||||
import platform
 | 
			
		||||
import re
 | 
			
		||||
import tempfile
 | 
			
		||||
from typing import Union
 | 
			
		||||
from urllib.parse import urlparse
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -219,8 +218,8 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]:
 | 
			
		||||
            int,
 | 
			
		||||
            int,
 | 
			
		||||
            int,
 | 
			
		||||
            Union[str, None],
 | 
			
		||||
            Union[tuple[str, int], tuple[str, int, int, int]],
 | 
			
		||||
            str | None,
 | 
			
		||||
            tuple[str, int] | tuple[str, int, int, int],
 | 
			
		||||
        ]
 | 
			
		||||
    ] = []
 | 
			
		||||
    for addr in address_list:
 | 
			
		||||
@@ -282,7 +281,7 @@ def read_file(path):
 | 
			
		||||
        raise EsphomeError(f"Error reading file {path}: {err}") from err
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _write_file(path: Union[Path, str], text: Union[str, bytes]):
 | 
			
		||||
def _write_file(path: Path | str, text: str | bytes):
 | 
			
		||||
    """Atomically writes `text` to the given path.
 | 
			
		||||
 | 
			
		||||
    Automatically creates all parent directories.
 | 
			
		||||
@@ -315,7 +314,7 @@ def _write_file(path: Union[Path, str], text: Union[str, bytes]):
 | 
			
		||||
                _LOGGER.error("Write file cleanup failed: %s", err)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_file(path: Union[Path, str], text: str):
 | 
			
		||||
def write_file(path: Path | str, text: str):
 | 
			
		||||
    try:
 | 
			
		||||
        _write_file(path, text)
 | 
			
		||||
    except OSError as err:
 | 
			
		||||
@@ -324,7 +323,7 @@ def write_file(path: Union[Path, str], text: str):
 | 
			
		||||
        raise EsphomeError(f"Could not write file at {path}") from err
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_file_if_changed(path: Union[Path, str], text: str) -> bool:
 | 
			
		||||
def write_file_if_changed(path: Path | str, text: str) -> bool:
 | 
			
		||||
    """Write text to the given path, but not if the contents match already.
 | 
			
		||||
 | 
			
		||||
    Returns true if the file was changed.
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user