mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 23:21:54 +00:00 
			
		
		
		
	Merge branch 'dev' into socket-client-mode
This commit is contained in:
		| @@ -1,56 +1,64 @@ | ||||
| { | ||||
|   "name": "ESPHome Dev", | ||||
|   "image": "esphome/esphome-lint:dev", | ||||
|   "image": "ghcr.io/esphome/esphome-lint:dev", | ||||
|   "postCreateCommand": [ | ||||
|     "script/devcontainer-post-create" | ||||
|   ], | ||||
|   "containerEnv": { | ||||
|     "DEVCONTAINER": "1" | ||||
|   }, | ||||
|   "runArgs": [ | ||||
|     "--privileged", | ||||
|     "-e", | ||||
|     "ESPHOME_DASHBOARD_USE_PING=1" | ||||
|   ], | ||||
|   "appPort": 6052, | ||||
|   "extensions": [ | ||||
|     // python | ||||
|     "ms-python.python", | ||||
|     "visualstudioexptteam.vscodeintellicode", | ||||
|     // yaml | ||||
|     "redhat.vscode-yaml", | ||||
|     // cpp | ||||
|     "ms-vscode.cpptools", | ||||
|     // editorconfig | ||||
|     "editorconfig.editorconfig", | ||||
|   ], | ||||
|   "settings": { | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.pythonPath": "/usr/bin/python3", | ||||
|     "python.linting.pylintEnabled": true, | ||||
|     "python.linting.enabled": true, | ||||
|     "python.formatting.provider": "black", | ||||
|     "editor.formatOnPaste": false, | ||||
|     "editor.formatOnSave": true, | ||||
|     "editor.formatOnType": true, | ||||
|     "files.trimTrailingWhitespace": true, | ||||
|     "terminal.integrated.defaultProfile.linux": "bash", | ||||
|     "yaml.customTags": [ | ||||
|       "!secret scalar", | ||||
|       "!lambda scalar", | ||||
|       "!include_dir_named scalar", | ||||
|       "!include_dir_list scalar", | ||||
|       "!include_dir_merge_list scalar", | ||||
|       "!include_dir_merge_named scalar" | ||||
|     ], | ||||
|     "files.exclude": { | ||||
|       "**/.git": true, | ||||
|       "**/.DS_Store": true, | ||||
|       "**/*.pyc": { | ||||
|         "when": "$(basename).py" | ||||
|       }, | ||||
|       "**/__pycache__": true | ||||
|     }, | ||||
|     "files.associations": { | ||||
|       "**/.vscode/*.json": "jsonc" | ||||
|     }, | ||||
|     "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", | ||||
|   "customizations": { | ||||
|     "vscode": { | ||||
|       "extensions": [ | ||||
|         // python | ||||
|         "ms-python.python", | ||||
|         "visualstudioexptteam.vscodeintellicode", | ||||
|         // yaml | ||||
|         "redhat.vscode-yaml", | ||||
|         // cpp | ||||
|         "ms-vscode.cpptools", | ||||
|         // editorconfig | ||||
|         "editorconfig.editorconfig", | ||||
|       ], | ||||
|       "settings": { | ||||
|         "python.languageServer": "Pylance", | ||||
|         "python.pythonPath": "/usr/bin/python3", | ||||
|         "python.linting.pylintEnabled": true, | ||||
|         "python.linting.enabled": true, | ||||
|         "python.formatting.provider": "black", | ||||
|         "editor.formatOnPaste": false, | ||||
|         "editor.formatOnSave": true, | ||||
|         "editor.formatOnType": true, | ||||
|         "files.trimTrailingWhitespace": true, | ||||
|         "terminal.integrated.defaultProfile.linux": "bash", | ||||
|         "yaml.customTags": [ | ||||
|           "!secret scalar", | ||||
|           "!lambda scalar", | ||||
|           "!extend scalar", | ||||
|           "!include_dir_named scalar", | ||||
|           "!include_dir_list scalar", | ||||
|           "!include_dir_merge_list scalar", | ||||
|           "!include_dir_merge_named scalar" | ||||
|         ], | ||||
|         "files.exclude": { | ||||
|           "**/.git": true, | ||||
|           "**/.DS_Store": true, | ||||
|           "**/*.pyc": { | ||||
|             "when": "$(basename).py" | ||||
|           }, | ||||
|           "**/__pycache__": true | ||||
|         }, | ||||
|         "files.associations": { | ||||
|           "**/.vscode/*.json": "jsonc" | ||||
|         }, | ||||
|         "C_Cpp.clang_format_path": "/usr/bin/clang-format-13" | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -25,10 +25,9 @@ indent_size = 2 | ||||
| [*.{yaml,yml}] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| quote_type = single | ||||
| quote_type = double | ||||
|  | ||||
| # JSON | ||||
| [*.json] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| # Normalize line endings to LF in the repository | ||||
| * text eol=lf | ||||
| *.png binary | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| --- | ||||
| # These are supported funding model platforms | ||||
|  | ||||
| custom: https://www.nabucasa.com | ||||
|   | ||||
							
								
								
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| --- | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: Issue Tracker | ||||
| @@ -5,8 +6,10 @@ contact_links: | ||||
|     about: Please create bug reports in the dedicated issue tracker. | ||||
|   - name: Feature Request Tracker | ||||
|     url: https://github.com/esphome/feature-requests | ||||
|     about: Please create feature requests in the dedicated feature request tracker. | ||||
|     about: | | ||||
|       Please create feature requests in the dedicated feature request tracker. | ||||
|   - name: Frequently Asked Question | ||||
|     url: https://esphome.io/guides/faq.html | ||||
|     about: Please view the FAQ for common questions and what to include in a bug report. | ||||
|      | ||||
|     about: | | ||||
|       Please view the FAQ for common questions and what | ||||
|       to include in a bug report. | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| # What does this implement/fix?  | ||||
| # What does this implement/fix? | ||||
|  | ||||
| Quick description and explanation of changes | ||||
| <!-- Quick description and explanation of changes --> | ||||
|  | ||||
| ## Types of changes | ||||
|  | ||||
| @@ -18,6 +18,7 @@ Quick description and explanation of changes | ||||
| - [ ] ESP32 | ||||
| - [ ] ESP32 IDF | ||||
| - [ ] ESP8266 | ||||
| - [ ] RP2040 | ||||
|  | ||||
| ## Example entry for `config.yaml`: | ||||
| <!-- | ||||
| @@ -35,6 +36,6 @@ Quick description and explanation of changes | ||||
| ## Checklist: | ||||
|   - [ ] The code change is tested and works locally. | ||||
|   - [ ] Tests have been added to verify that the new code works (under `tests/` folder). | ||||
|    | ||||
|  | ||||
| If user exposed functionality or configuration variables are added/changed: | ||||
|   - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). | ||||
|   | ||||
							
								
								
									
										38
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| name: Restore Python | ||||
| inputs: | ||||
|   python-version: | ||||
|     description: Python version to restore | ||||
|     required: true | ||||
|     type: string | ||||
|   cache-key: | ||||
|     description: Cache key to use | ||||
|     required: true | ||||
|     type: string | ||||
| outputs: | ||||
|   python-version: | ||||
|     description: Python version restored | ||||
|     value: ${{ steps.python.outputs.python-version }} | ||||
| runs: | ||||
|   using: "composite" | ||||
|   steps: | ||||
|     - name: Set up Python ${{ inputs.python-version }} | ||||
|       id: python | ||||
|       uses: actions/setup-python@v4.6.0 | ||||
|       with: | ||||
|         python-version: ${{ inputs.python-version }} | ||||
|     - name: Restore Python virtual environment | ||||
|       id: cache-venv | ||||
|       uses: actions/cache/restore@v3.3.1 | ||||
|       with: | ||||
|         path: venv | ||||
|         # yamllint disable-line rule:line-length | ||||
|         key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }} | ||||
|     - name: Create Python virtual environment | ||||
|       if: steps.cache-venv.outputs.cache-hit != 'true' | ||||
|       shell: bash | ||||
|       run: | | ||||
|         python -m venv venv | ||||
|         . venv/bin/activate | ||||
|         python --version | ||||
|         pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|         pip install -e . | ||||
							
								
								
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,9 +1,15 @@ | ||||
| --- | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "pip" | ||||
|   - package-ecosystem: pip | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "daily" | ||||
|       interval: daily | ||||
|     ignore: | ||||
|       # Hypotehsis is only used for testing and is updated quite often | ||||
|       - dependency-name: hypothesis | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|   | ||||
							
								
								
									
										64
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										64
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,53 +1,63 @@ | ||||
| --- | ||||
| name: CI for docker images | ||||
|  | ||||
| # Only run when docker paths change | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev, beta, release] | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|       - "docker/**" | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|       - "script/platformio_install_deps.py" | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|       - "docker/**" | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|       - "script/platformio_install_deps.py" | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|   packages: read | ||||
|  | ||||
| concurrency: | ||||
|   # yamllint disable-line rule:line-length | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   check-docker: | ||||
|     name: Build docker containers | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|         echo "TAG=check" >> $GITHUB_ENV | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|           echo "TAG=check" >> $GITHUB_ENV | ||||
|  | ||||
|     - name: Run build | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${TAG}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build | ||||
|       - name: Run build | ||||
|         run: | | ||||
|           docker/build.py \ | ||||
|             --tag "${TAG}" \ | ||||
|             --arch "${{ matrix.arch }}" \ | ||||
|             --build-type "${{ matrix.build_type }}" \ | ||||
|             build | ||||
|   | ||||
							
								
								
									
										386
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										386
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,54 +1,275 @@ | ||||
| --- | ||||
| name: CI | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev, beta, release] | ||||
|  | ||||
|   pull_request: | ||||
|   merge_group: | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
|  | ||||
| env: | ||||
|   DEFAULT_PYTHON: "3.9" | ||||
|   PYUPGRADE_TARGET: "--py39-plus" | ||||
|   CLANG_FORMAT_VERSION: "13.0.1" | ||||
|  | ||||
| concurrency: | ||||
|   # yamllint disable-line rule:line-length | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| jobs: | ||||
|   ci: | ||||
|     name: ${{ matrix.name }} | ||||
|   common: | ||||
|     name: Create common environment | ||||
|     runs-on: ubuntu-latest | ||||
|     outputs: | ||||
|       cache-key: ${{ steps.cache-key.outputs.key }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Generate cache-key | ||||
|         id: cache-key | ||||
|         run: echo key="${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT | ||||
|       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||
|         id: python | ||||
|         uses: actions/setup-python@v4.6.0 | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|       - name: Restore Python virtual environment | ||||
|         id: cache-venv | ||||
|         uses: actions/cache@v3.3.1 | ||||
|         with: | ||||
|           path: venv | ||||
|           # yamllint disable-line rule:line-length | ||||
|           key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ steps.cache-key.outputs.key }} | ||||
|       - name: Create Python virtual environment | ||||
|         if: steps.cache-venv.outputs.cache-hit != 'true' | ||||
|         run: | | ||||
|           python -m venv venv | ||||
|           . venv/bin/activate | ||||
|           python --version | ||||
|           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|           pip install -e . | ||||
|  | ||||
|   yamllint: | ||||
|     name: yamllint | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Run yamllint | ||||
|         uses: frenck/action-yamllint@v1.4.1 | ||||
|  | ||||
|   black: | ||||
|     name: Check black | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run black | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           black --verbose esphome tests | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   flake8: | ||||
|     name: Check flake8 | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run flake8 | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           flake8 esphome | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   pylint: | ||||
|     name: Check pylint | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run pylint | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           pylint -f parseable --persistent=n esphome | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   pyupgrade: | ||||
|     name: Check pyupgrade | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Run pyupgrade | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f` | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   ci-custom: | ||||
|     name: Run script/ci-custom | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Register matcher | ||||
|         run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||
|       - name: Run script/ci-custom | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           script/ci-custom.py | ||||
|           script/build_codeowners.py --check | ||||
|  | ||||
|   pytest: | ||||
|     name: Run pytest | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Register matcher | ||||
|         run: echo "::add-matcher::.github/workflows/matchers/pytest.json" | ||||
|       - name: Run pytest | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --tb=native tests | ||||
|  | ||||
|   clang-format: | ||||
|     name: Check clang-format | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Install clang-format | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           pip install clang-format==${{ env.CLANG_FORMAT_VERSION }} | ||||
|       - name: Run clang-format | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           script/clang-format -i | ||||
|           git diff-index --quiet HEAD -- | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   compile-tests: | ||||
|     name: Run YAML test ${{ matrix.file }} | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - yamllint | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 2 | ||||
|       matrix: | ||||
|         file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8] | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache@v3.3.1 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           # yamllint disable-line rule:line-length | ||||
|           key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }} | ||||
|       - name: Run esphome compile tests/test${{ matrix.file }}.yaml | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           esphome compile tests/test${{ matrix.file }}.yaml | ||||
|  | ||||
|   clang-tidy: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - yamllint | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 2 | ||||
|       matrix: | ||||
|         include: | ||||
|           - id: ci-custom | ||||
|             name: Run script/ci-custom | ||||
|           - id: lint-python | ||||
|             name: Run script/lint-python | ||||
|           - id: test | ||||
|             file: tests/test1.yaml | ||||
|             name: Test tests/test1.yaml | ||||
|             pio_cache_key: test1 | ||||
|           - id: test | ||||
|             file: tests/test2.yaml | ||||
|             name: Test tests/test2.yaml | ||||
|             pio_cache_key: test2 | ||||
|           - id: test | ||||
|             file: tests/test3.yaml | ||||
|             name: Test tests/test3.yaml | ||||
|             pio_cache_key: test3 | ||||
|           - id: test | ||||
|             file: tests/test4.yaml | ||||
|             name: Test tests/test4.yaml | ||||
|             pio_cache_key: test4 | ||||
|           - id: test | ||||
|             file: tests/test5.yaml | ||||
|             name: Test tests/test5.yaml | ||||
|             pio_cache_key: test5 | ||||
|           - id: pytest | ||||
|             name: Run pytest | ||||
|           - id: clang-format | ||||
|             name: Run script/clang-format | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP8266 | ||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||
| @@ -75,92 +296,61 @@ jobs: | ||||
|             pio_cache_key: tidyesp32-idf | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         id: python | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@v3.5.2 | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|  | ||||
|       - name: Cache virtualenv | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: .venv | ||||
|           key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} | ||||
|           restore-keys: | | ||||
|             venv-${{ steps.python.outputs.python-version }}- | ||||
|  | ||||
|       - name: Set up virtualenv | ||||
|         run: | | ||||
|           python -m venv .venv | ||||
|           source .venv/bin/activate | ||||
|           pip install -U pip | ||||
|           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|           pip install -e . | ||||
|           echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH | ||||
|           echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV | ||||
|  | ||||
|       # Use per check platformio cache because checks use different parts | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache@v2 | ||||
|         uses: actions/cache@v3.3.1 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           # yamllint disable-line rule:line-length | ||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||
|         if: matrix.id == 'test' || matrix.id == 'clang-tidy' | ||||
|  | ||||
|       - name: Install clang tools | ||||
|         run: | | ||||
|           sudo apt-get install \ | ||||
|               clang-format-11 \ | ||||
|               clang-tidy-11 | ||||
|         if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' | ||||
|       - name: Install clang-tidy | ||||
|         run: sudo apt-get install clang-tidy-11 | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/lint-python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/pytest.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|  | ||||
|       - name: Lint Custom | ||||
|         run: | | ||||
|           script/ci-custom.py | ||||
|           script/build_codeowners.py --check | ||||
|         if: matrix.id == 'ci-custom' | ||||
|  | ||||
|       - name: Lint Python | ||||
|         run: script/lint-python | ||||
|         if: matrix.id == 'lint-python' | ||||
|  | ||||
|       - run: esphome compile ${{ matrix.file }} | ||||
|         if: matrix.id == 'test' | ||||
|         env: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|       - name: Run pytest | ||||
|         run: | | ||||
|           pytest -vv --tb=native tests | ||||
|         if: matrix.id == 'pytest' | ||||
|  | ||||
|       # Also run git-diff-index so that the step is marked as failed on formatting errors, | ||||
|       # since clang-format doesn't do anything but change files if -i is passed. | ||||
|       - name: Run clang-format | ||||
|         run: | | ||||
|           script/clang-format -i | ||||
|           git diff-index --quiet HEAD -- | ||||
|         if: matrix.id == 'clang-format' | ||||
|  | ||||
|       - name: Run clang-tidy | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           script/clang-tidy --all-headers --fix ${{ matrix.options }} | ||||
|         if: matrix.id == 'clang-tidy' | ||||
|         env: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format') | ||||
|         # yamllint disable-line rule:line-length | ||||
|         if: always() | ||||
|  | ||||
|   ci-status: | ||||
|     name: CI Status | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: | ||||
|       - common | ||||
|       - black | ||||
|       - ci-custom | ||||
|       - clang-format | ||||
|       - flake8 | ||||
|       - pylint | ||||
|       - pytest | ||||
|       - pyupgrade | ||||
|       - yamllint | ||||
|       - compile-tests | ||||
|       - clang-tidy | ||||
|     if: always() | ||||
|     steps: | ||||
|       - name: Success | ||||
|         if: ${{ !(contains(needs.*.result, 'failure')) }} | ||||
|         run: exit 0 | ||||
|       - name: Failure | ||||
|         if: ${{ contains(needs.*.result, 'failure') }} | ||||
|         run: exit 1 | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,10 @@ | ||||
| --- | ||||
| name: Lock | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|     - cron: "30 0 * * *" | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
| @@ -16,7 +18,7 @@ jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@v3 | ||||
|       - uses: dessant/lock-threads@v4 | ||||
|         with: | ||||
|           pr-inactive-days: "1" | ||||
|           pr-lock-reason: "" | ||||
|   | ||||
							
								
								
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| --- | ||||
| name: Publish Release | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   release: | ||||
| @@ -17,9 +19,10 @@ jobs: | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.tag }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         # yamllint disable rule:line-length | ||||
|         run: | | ||||
|           if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then | ||||
|             TAG="${GITHUB_REF#refs/tags/}" | ||||
| @@ -27,23 +30,30 @@ jobs: | ||||
|             TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") | ||||
|             today="$(date --utc '+%Y%m%d')" | ||||
|             TAG="${TAG}${today}" | ||||
|             BRANCH=${GITHUB_REF#refs/heads/} | ||||
|             if [[ "$BRANCH" != "dev" ]]; then | ||||
|               TAG="${TAG}-${BRANCH}" | ||||
|             fi | ||||
|           fi | ||||
|           echo "::set-output name=tag::${TAG}" | ||||
|           echo "tag=${TAG}" >> $GITHUB_OUTPUT | ||||
|         # yamllint enable rule:line-length | ||||
|  | ||||
|   deploy-pypi: | ||||
|     name: Build and publish to PyPi | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v1 | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: '3.x' | ||||
|           python-version: "3.x" | ||||
|       - name: Set up python environment | ||||
|         env: | ||||
|           ESPHOME_NO_VENV: 1 | ||||
|         run: | | ||||
|           script/setup | ||||
|           pip install setuptools wheel twine | ||||
|           pip install twine | ||||
|       - name: Build | ||||
|         run: python setup.py sdist bdist_wheel | ||||
|       - name: Upload | ||||
| @@ -53,102 +63,95 @@ jobs: | ||||
|         run: twine upload dist/* | ||||
|  | ||||
|   deploy-docker: | ||||
|     name: Build and publish docker containers | ||||
|     name: Build and publish ESPHome ${{ matrix.image.title}} | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|     runs-on: ubuntu-latest | ||||
|     continue-on-error: ${{ matrix.image.title == 'lint' }} | ||||
|     needs: [init] | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|         image: | ||||
|           - title: "ha-addon" | ||||
|             suffix: "hassio" | ||||
|             target: "hassio" | ||||
|             baseimg: "hassio" | ||||
|           - title: "docker" | ||||
|             suffix: "" | ||||
|             target: "docker" | ||||
|             baseimg: "docker" | ||||
|           - title: "lint" | ||||
|             suffix: "lint" | ||||
|             target: "lint" | ||||
|             baseimg: "docker" | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|  | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|         username: ${{ secrets.DOCKER_USER }} | ||||
|         password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|     - name: Log in to the GitHub container registry | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|     - name: Build and push | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build \ | ||||
|           --push | ||||
|       - name: Generate short tags | ||||
|         id: tags | ||||
|         run: | | ||||
|           docker/generate_tags.py \ | ||||
|             --tag "${{ needs.init.outputs.tag }}" \ | ||||
|             --suffix "${{ matrix.image.suffix }}" | ||||
|  | ||||
|   deploy-docker-manifest: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     permissions: | ||||
|       contents: read | ||||
|       packages: write | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [init, deploy-docker] | ||||
|     strategy: | ||||
|       matrix: | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Enable experimental manifest support | ||||
|       run: | | ||||
|         mkdir -p ~/.docker | ||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|       - name: Build and push | ||||
|         uses: docker/build-push-action@v4 | ||||
|         with: | ||||
|           context: . | ||||
|           file: ./docker/Dockerfile | ||||
|           platforms: linux/amd64,linux/arm/v7,linux/arm64 | ||||
|           target: ${{ matrix.image.target }} | ||||
|           push: true | ||||
|           # yamllint disable rule:line-length | ||||
|           cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} | ||||
|           cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max | ||||
|           # yamllint enable rule:line-length | ||||
|           tags: ${{ steps.tags.outputs.tags }} | ||||
|           build-args: | | ||||
|             BASEIMGTYPE=${{ matrix.image.baseimg }} | ||||
|             BUILD_VERSION=${{ needs.init.outputs.tag }} | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|         username: ${{ secrets.DOCKER_USER }} | ||||
|         password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|     - name: Log in to the GitHub container registry | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|     - name: Run manifest | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           manifest | ||||
|  | ||||
|   deploy-hassio-repo: | ||||
|   deploy-ha-addon-repo: | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [deploy-docker] | ||||
|     steps: | ||||
|       - env: | ||||
|           TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} | ||||
|         run: | | ||||
|           TAG="${GITHUB_REF#refs/tags/}" | ||||
|           curl \ | ||||
|             -u ":$TOKEN" \ | ||||
|             -X POST \ | ||||
|             -H "Accept: application/vnd.github.v3+json" \ | ||||
|             https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ | ||||
|             -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" | ||||
|       - name: Trigger Workflow | ||||
|         uses: actions/github-script@v6 | ||||
|         with: | ||||
|           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||
|           script: | | ||||
|             github.rest.actions.createWorkflowDispatch({ | ||||
|               owner: "esphome", | ||||
|               repo: "home-assistant-addon", | ||||
|               workflow_id: "bump-version.yml", | ||||
|               ref: "main", | ||||
|               inputs: { | ||||
|                 version: "${{ github.event.release.tag_name }}", | ||||
|                 content: ${{ toJSON(github.event.release.body) }} | ||||
|               } | ||||
|             }) | ||||
|   | ||||
							
								
								
									
										13
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,10 @@ | ||||
| --- | ||||
| name: Stale | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|     - cron: "30 0 * * *" | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
| @@ -16,7 +18,7 @@ jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|       - uses: actions/stale@v8 | ||||
|         with: | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
| @@ -24,18 +26,19 @@ jobs: | ||||
|           days-before-issue-close: -1 | ||||
|           remove-stale-when-updated: true | ||||
|           stale-pr-label: "stale" | ||||
|           exempt-pr-labels: "no-stale" | ||||
|           exempt-pr-labels: "not-stale" | ||||
|           stale-pr-message: > | ||||
|             There hasn't been any activity on this pull request recently. This | ||||
|             pull request has been automatically marked as stale because of that | ||||
|             and will be closed if no further activity occurs within 7 days. | ||||
|             Thank you for your contributions. | ||||
|  | ||||
|   # Use stale to automatically close issues with a reference to the issue tracker | ||||
|   # Use stale to automatically close issues with a | ||||
|   # reference to the issue tracker | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|       - uses: actions/stale@v8 | ||||
|         with: | ||||
|           days-before-pr-stale: -1 | ||||
|           days-before-pr-close: -1 | ||||
|   | ||||
							
								
								
									
										49
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| --- | ||||
| name: Synchronise Device Classes from Home Assistant | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   schedule: | ||||
|     - cron: '45 6 * * *' | ||||
|  | ||||
|  | ||||
| jobs: | ||||
|   sync: | ||||
|     name: Sync Device Classes | ||||
|     runs-on: ubuntu-latest | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     steps: | ||||
|       - name: Checkout | ||||
|         uses: actions/checkout@v3 | ||||
|  | ||||
|       - name: Checkout Home Assistant | ||||
|         uses: actions/checkout@v3 | ||||
|         with: | ||||
|           repository: home-assistant/core | ||||
|           path: lib/home-assistant | ||||
|  | ||||
|       - name: Setup Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: 3.11 | ||||
|  | ||||
|       - name: Install Home Assistant | ||||
|         run: | | ||||
|           python -m pip install --upgrade pip | ||||
|           pip install -e lib/home-assistant | ||||
|  | ||||
|       - name: Sync | ||||
|         run: | | ||||
|           python ./script/sync-device_class.py | ||||
|  | ||||
|       - name: Commit changes | ||||
|         uses: peter-evans/create-pull-request@v5 | ||||
|         with: | ||||
|           commit-message: "Synchronise Device Classes from Home Assistant" | ||||
|           committer: esphomebot <esphome@nabucasa.com> | ||||
|           author: esphomebot <esphome@nabucasa.com> | ||||
|           branch: sync/device-classes | ||||
|           delete-branch: true | ||||
|           title: "Synchronise Device Classes from Home Assistant" | ||||
|           body-path: .github/PULL_REQUEST_TEMPLATE.md | ||||
|           token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -77,6 +77,7 @@ venv/ | ||||
| ENV/ | ||||
| env.bak/ | ||||
| venv.bak/ | ||||
| venv-*/ | ||||
|  | ||||
| # mypy | ||||
| .mypy_cache/ | ||||
| @@ -127,3 +128,5 @@ tests/.esphome/ | ||||
|  | ||||
| sdkconfig.* | ||||
| !sdkconfig.defaults | ||||
|  | ||||
| .tests/ | ||||
| @@ -1,6 +0,0 @@ | ||||
| ports: | ||||
| - port: 6052 | ||||
|   onOpen: open-preview | ||||
| tasks: | ||||
| - before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup | ||||
|   command: python -m esphome dashboard config | ||||
| @@ -1,16 +1,17 @@ | ||||
| --- | ||||
| # See https://pre-commit.com for more information | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/ambv/black | ||||
|     rev: 22.1.0 | ||||
|   - repo: https://github.com/psf/black | ||||
|     rev: 23.3.0 | ||||
|     hooks: | ||||
|     - id: black | ||||
|       args: | ||||
|         - --safe | ||||
|         - --quiet | ||||
|       files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|   - repo: https://gitlab.com/pycqa/flake8 | ||||
|     rev: 4.0.1 | ||||
|       - id: black | ||||
|         args: | ||||
|           - --safe | ||||
|           - --quiet | ||||
|         files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|   - repo: https://github.com/PyCQA/flake8 | ||||
|     rev: 6.0.0 | ||||
|     hooks: | ||||
|       - id: flake8 | ||||
|         additional_dependencies: | ||||
| @@ -25,3 +26,8 @@ repos: | ||||
|           - --branch=dev | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v3.7.0 | ||||
|     hooks: | ||||
|       - id: pyupgrade | ||||
|         args: [--py39-plus] | ||||
|   | ||||
							
								
								
									
										33
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										33
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -2,15 +2,24 @@ | ||||
|   "version": "2.0.0", | ||||
|   "tasks": [ | ||||
|     { | ||||
|       "label": "run", | ||||
|       "label": "Run Dashboard", | ||||
|       "type": "shell", | ||||
|       "command": "python3 -m esphome dashboard config/", | ||||
|       "command": "${command:python.interpreterPath}", | ||||
|       "args": [ | ||||
|         "-m", | ||||
|         "esphome", | ||||
|         "dashboard", | ||||
|         "config/" | ||||
|       ], | ||||
|       "problemMatcher": [] | ||||
|     }, | ||||
|     { | ||||
|       "label": "clang-tidy", | ||||
|       "type": "shell", | ||||
|       "command": "./script/clang-tidy", | ||||
|       "command": "${command:python.interpreterPath}", | ||||
|       "args": [ | ||||
|         "./script/clang-tidy" | ||||
|       ], | ||||
|       "problemMatcher": [ | ||||
|         { | ||||
|           "owner": "clang-tidy", | ||||
| @@ -27,6 +36,24 @@ | ||||
|           ] | ||||
|         } | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "label": "Generate proto files", | ||||
|       "type": "shell", | ||||
|       "command": "${command:python.interpreterPath}", | ||||
|       "args": [ | ||||
|         "./script/api_protobuf/api_protobuf.py" | ||||
|       ], | ||||
|       "group": { | ||||
|         "kind": "build", | ||||
|         "isDefault": true | ||||
|       }, | ||||
|       "presentation": { | ||||
|         "reveal": "never", | ||||
|         "close": true, | ||||
|         "panel": "new" | ||||
|       }, | ||||
|       "problemMatcher": [] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
							
								
								
									
										122
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										122
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -11,27 +11,43 @@ esphome/*.py @esphome/core | ||||
| esphome/core/* @esphome/core | ||||
|  | ||||
| # Integrations | ||||
| esphome/components/absolute_humidity/* @DAVe3283 | ||||
| esphome/components/ac_dimmer/* @glmnet | ||||
| esphome/components/adc/* @esphome/core | ||||
| esphome/components/adc128s102/* @DeerMaximum | ||||
| esphome/components/addressable_light/* @justfalter | ||||
| esphome/components/airthings_ble/* @jeromelaban | ||||
| esphome/components/airthings_wave_base/* @jeromelaban @ncareau | ||||
| esphome/components/airthings_wave_mini/* @ncareau | ||||
| esphome/components/airthings_wave_plus/* @jeromelaban | ||||
| esphome/components/alarm_control_panel/* @grahambrown11 | ||||
| esphome/components/am43/* @buxtronix | ||||
| esphome/components/am43/cover/* @buxtronix | ||||
| esphome/components/am43/sensor/* @buxtronix | ||||
| esphome/components/analog_threshold/* @ianchi | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| esphome/components/api/* @OttoWinter | ||||
| esphome/components/as7341/* @mrgnr | ||||
| esphome/components/async_tcp/* @OttoWinter | ||||
| esphome/components/atc_mithermometer/* @ahpohl | ||||
| esphome/components/b_parasite/* @rbaron | ||||
| esphome/components/ballu/* @bazuchan | ||||
| esphome/components/bang_bang/* @OttoWinter | ||||
| esphome/components/bedjet/* @jhansche | ||||
| esphome/components/bedjet/climate/* @jhansche | ||||
| esphome/components/bedjet/fan/* @jhansche | ||||
| esphome/components/bh1750/* @OttoWinter | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/bl0942/* @dbuezas | ||||
| esphome/components/ble_client/* @buxtronix | ||||
| esphome/components/bluetooth_proxy/* @jesserockz | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/bmp3xx/* @martgras | ||||
| esphome/components/bp1658cj/* @Cossid | ||||
| esphome/components/bp5758d/* @Cossid | ||||
| esphome/components/button/* @esphome/core | ||||
| esphome/components/canbus/* @danielschramm @mvturnho | ||||
| esphome/components/cap1188/* @MrEditor97 | ||||
| @@ -42,56 +58,98 @@ esphome/components/climate/* @esphome/core | ||||
| esphome/components/climate_ir/* @glmnet | ||||
| esphome/components/color_temperature/* @jesserockz | ||||
| esphome/components/coolix/* @glmnet | ||||
| esphome/components/copy/* @OttoWinter | ||||
| esphome/components/cover/* @esphome/core | ||||
| esphome/components/cs5460a/* @balrog-kun | ||||
| esphome/components/cse7761/* @berfenger | ||||
| esphome/components/ct_clamp/* @jesserockz | ||||
| esphome/components/current_based/* @djwmarcx | ||||
| esphome/components/dac7678/* @NickB1 | ||||
| esphome/components/daikin_brc/* @hagak | ||||
| esphome/components/daly_bms/* @s1lvi0 | ||||
| esphome/components/dashboard_import/* @esphome/core | ||||
| esphome/components/debug/* @OttoWinter | ||||
| esphome/components/delonghi/* @grob6000 | ||||
| esphome/components/dfplayer/* @glmnet | ||||
| esphome/components/dht/* @OttoWinter | ||||
| esphome/components/display_menu_base/* @numo68 | ||||
| esphome/components/dps310/* @kbx81 | ||||
| esphome/components/ds1307/* @badbadc0ffee | ||||
| esphome/components/dsmr/* @glmnet @zuidwijk | ||||
| esphome/components/ee895/* @Stock-M | ||||
| esphome/components/ektf2232/* @jesserockz | ||||
| esphome/components/ens210/* @itn3rd77 | ||||
| esphome/components/esp32/* @esphome/core | ||||
| esphome/components/esp32_ble/* @jesserockz | ||||
| esphome/components/esp32_ble_client/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @jesserockz | ||||
| esphome/components/esp32_camera_web_server/* @ayufan | ||||
| esphome/components/esp32_can/* @Sympatron | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/ethernet_info/* @gtjadsonsantos | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| esphome/components/ezo/* @ssieb | ||||
| esphome/components/ezo_pmp/* @carlos-sarmiento | ||||
| esphome/components/factory_reset/* @anatoly-savchenkov | ||||
| esphome/components/fastled_base/* @OttoWinter | ||||
| esphome/components/feedback/* @ianchi | ||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||
| esphome/components/fs3000/* @kahrendt | ||||
| esphome/components/globals/* @esphome/core | ||||
| esphome/components/gp8403/* @jesserockz | ||||
| esphome/components/gpio/* @esphome/core | ||||
| esphome/components/gps/* @coogle | ||||
| esphome/components/graph/* @synco | ||||
| esphome/components/growatt_solar/* @leeuwte | ||||
| esphome/components/haier/* @paveldn | ||||
| esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/hm3301/* @freekode | ||||
| esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/honeywellabp/* @RubyBailey | ||||
| esphome/components/host/* @esphome/core | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/hte501/* @Stock-M | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/hyt271/* @Philippe12 | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/i2s_audio/* @jesserockz | ||||
| esphome/components/i2s_audio/media_player/* @jesserockz | ||||
| esphome/components/i2s_audio/microphone/* @jesserockz | ||||
| esphome/components/i2s_audio/speaker/* @jesserockz | ||||
| esphome/components/ili9xxx/* @nielsnl68 | ||||
| esphome/components/improv_base/* @esphome/core | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| esphome/components/ina260/* @MrEditor97 | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| esphome/components/inkplate6/* @jesserockz | ||||
| esphome/components/integration/* @OttoWinter | ||||
| esphome/components/internal_temperature/* @Mat931 | ||||
| esphome/components/interval/* @esphome/core | ||||
| esphome/components/json/* @OttoWinter | ||||
| esphome/components/kalman_combinator/* @Cat-Ion | ||||
| esphome/components/key_collector/* @ssieb | ||||
| esphome/components/key_provider/* @ssieb | ||||
| esphome/components/kuntze/* @ssieb | ||||
| esphome/components/lcd_menu/* @numo68 | ||||
| esphome/components/ld2410/* @sebcaps | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/light/* @esphome/core | ||||
| esphome/components/lilygo_t5_47/touchscreen/* @jesserockz | ||||
| esphome/components/lock/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/matrix_keypad/* @ssieb | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
| esphome/components/max6956/* @looping40 | ||||
| esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/max9611/* @mckaymatthew | ||||
| esphome/components/mcp23008/* @jesserockz | ||||
| esphome/components/mcp23017/* @jesserockz | ||||
| esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | ||||
| @@ -101,20 +159,34 @@ esphome/components/mcp23x17_base/* @jesserockz | ||||
| esphome/components/mcp23xxx_base/* @jesserockz | ||||
| esphome/components/mcp2515/* @danielschramm @mvturnho | ||||
| esphome/components/mcp3204/* @rsumner | ||||
| esphome/components/mcp4728/* @berfenger | ||||
| esphome/components/mcp47a1/* @jesserockz | ||||
| esphome/components/mcp9600/* @MrEditor97 | ||||
| esphome/components/mcp9808/* @k7hpn | ||||
| esphome/components/md5/* @esphome/core | ||||
| esphome/components/mdns/* @esphome/core | ||||
| esphome/components/media_player/* @jesserockz | ||||
| esphome/components/microphone/* @jesserockz | ||||
| esphome/components/mics_4514/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/mlx90393/* @functionpointer | ||||
| esphome/components/mlx90614/* @jesserockz | ||||
| esphome/components/mmc5603/* @benhoff | ||||
| esphome/components/modbus_controller/* @martgras | ||||
| esphome/components/modbus_controller/binary_sensor/* @martgras | ||||
| esphome/components/modbus_controller/number/* @martgras | ||||
| esphome/components/modbus_controller/output/* @martgras | ||||
| esphome/components/modbus_controller/select/* @martgras @stegm | ||||
| esphome/components/modbus_controller/sensor/* @martgras | ||||
| esphome/components/modbus_controller/switch/* @martgras | ||||
| esphome/components/modbus_controller/text_sensor/* @martgras | ||||
| esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan | ||||
| esphome/components/mopeka_pro_check/* @spbrogan | ||||
| esphome/components/mopeka_std_check/* @Fabian-Schmidt | ||||
| esphome/components/mpl3115a2/* @kbickar | ||||
| esphome/components/mpu6886/* @fabaff | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @senexcrenshaw | ||||
| esphome/components/nextion/binary_sensor/* @senexcrenshaw | ||||
| @@ -125,6 +197,9 @@ esphome/components/nfc/* @jesserockz | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/pca6416a/* @Mat931 | ||||
| esphome/components/pca9554/* @hwstar | ||||
| esphome/components/pcf85063/* @brogon | ||||
| esphome/components/pid/* @OttoWinter | ||||
| esphome/components/pipsolar/* @andreashergert1984 | ||||
| esphome/components/pm1006/* @habbie | ||||
| @@ -135,31 +210,53 @@ esphome/components/pn532_spi/* @OttoWinter @jesserockz | ||||
| esphome/components/power_supply/* @esphome/core | ||||
| esphome/components/preferences/* @esphome/core | ||||
| esphome/components/psram/* @esphome/core | ||||
| esphome/components/pulse_meter/* @stevebaxter | ||||
| esphome/components/pulse_meter/* @cstaahl @stevebaxter | ||||
| esphome/components/pvvx_mithermometer/* @pasiz | ||||
| esphome/components/qmp6988/* @andrewpc | ||||
| esphome/components/qr_code/* @wjtje | ||||
| esphome/components/radon_eye_ble/* @jeffeb3 | ||||
| esphome/components/radon_eye_rd200/* @jeffeb3 | ||||
| esphome/components/rc522/* @glmnet | ||||
| esphome/components/rc522_i2c/* @glmnet | ||||
| esphome/components/rc522_spi/* @glmnet | ||||
| esphome/components/restart/* @esphome/core | ||||
| esphome/components/rf_bridge/* @jesserockz | ||||
| esphome/components/rgbct/* @jesserockz | ||||
| esphome/components/rp2040/* @jesserockz | ||||
| esphome/components/rp2040_pio_led_strip/* @Papa-DMan | ||||
| esphome/components/rp2040_pwm/* @jesserockz | ||||
| esphome/components/rtttl/* @glmnet | ||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | ||||
| esphome/components/scd4x/* @sjtrny | ||||
| esphome/components/scd4x/* @martgras @sjtrny | ||||
| esphome/components/script/* @esphome/core | ||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||
| esphome/components/sdp3x/* @Azimath | ||||
| esphome/components/selec_meter/* @sourabhjaiswal | ||||
| esphome/components/select/* @esphome/core | ||||
| esphome/components/sen21231/* @shreyaskarnik | ||||
| esphome/components/sen5x/* @martgras | ||||
| esphome/components/sensirion_common/* @martgras | ||||
| esphome/components/sensor/* @esphome/core | ||||
| esphome/components/sgp40/* @SenexCrenshaw | ||||
| esphome/components/sgp4x/* @SenexCrenshaw @martgras | ||||
| esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||
| esphome/components/sht4x/* @sjtrny | ||||
| esphome/components/shutdown/* @esphome/core @jsuanet | ||||
| esphome/components/sigma_delta_output/* @Cat-Ion | ||||
| esphome/components/sim800l/* @glmnet | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 | ||||
| esphome/components/sm10bit_base/* @Cossid | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 @dd32 @matika77 | ||||
| esphome/components/sm2235/* @Cossid | ||||
| esphome/components/sm2335/* @Cossid | ||||
| esphome/components/sml/* @alengwenus | ||||
| esphome/components/smt100/* @piechade | ||||
| esphome/components/sn74hc165/* @jesserockz | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||
| esphome/components/speaker/* @jesserockz | ||||
| esphome/components/spi/* @esphome/core | ||||
| esphome/components/sprinkler/* @kbx81 | ||||
| esphome/components/sps30/* @martgras | ||||
| esphome/components/ssd1322_base/* @kbx81 | ||||
| esphome/components/ssd1322_spi/* @kbx81 | ||||
| esphome/components/ssd1325_base/* @kbx81 | ||||
| @@ -180,12 +277,18 @@ esphome/components/switch/* @esphome/core | ||||
| esphome/components/t6615/* @tylermenezes | ||||
| esphome/components/tca9548a/* @andreashergert1984 | ||||
| esphome/components/tcl112/* @glmnet | ||||
| esphome/components/tee501/* @Stock-M | ||||
| esphome/components/teleinfo/* @0hax | ||||
| esphome/components/template/alarm_control_panel/* @grahambrown11 | ||||
| esphome/components/thermostat/* @kbx81 | ||||
| esphome/components/time/* @OttoWinter | ||||
| esphome/components/tlc5947/* @rnauber | ||||
| esphome/components/tm1621/* @Philippe12 | ||||
| esphome/components/tm1637/* @glmnet | ||||
| esphome/components/tm1638/* @skykingjwc | ||||
| esphome/components/tm1651/* @freekode | ||||
| esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp1075/* @sybrenstuvel | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| esphome/components/toshiba/* @kbx81 | ||||
| @@ -194,16 +297,27 @@ esphome/components/tsl2591/* @wjcarpenter | ||||
| esphome/components/tuya/binary_sensor/* @jesserockz | ||||
| esphome/components/tuya/climate/* @jesserockz | ||||
| esphome/components/tuya/number/* @frankiboy1 | ||||
| esphome/components/tuya/select/* @bearpawmaxim | ||||
| esphome/components/tuya/sensor/* @jesserockz | ||||
| esphome/components/tuya/switch/* @jesserockz | ||||
| esphome/components/tuya/text_sensor/* @dentra | ||||
| esphome/components/uart/* @esphome/core | ||||
| esphome/components/ufire_ec/* @pvizeli | ||||
| esphome/components/ufire_ise/* @pvizeli | ||||
| esphome/components/ultrasonic/* @OttoWinter | ||||
| esphome/components/vbus/* @ssieb | ||||
| esphome/components/version/* @esphome/core | ||||
| esphome/components/voice_assistant/* @jesserockz | ||||
| esphome/components/wake_on_lan/* @willwill2will54 | ||||
| esphome/components/web_server_base/* @OttoWinter | ||||
| esphome/components/whirlpool/* @glmnet | ||||
| esphome/components/whynter/* @aeonsablaze | ||||
| esphome/components/wiegand/* @ssieb | ||||
| esphome/components/wl_134/* @hobbypunk90 | ||||
| esphome/components/x9c/* @EtienneMD | ||||
| esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
| esphome/components/xpt2046/* @numo68 | ||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/* @nielsnl68 @numo68 | ||||
|   | ||||
| @@ -5,7 +5,7 @@ For a detailed guide, please see https://esphome.io/guides/contributing.html#con | ||||
| Things to note when contributing: | ||||
|  | ||||
|  - Please test your changes :) | ||||
|  - If a new feature is added or an existing user-facing feature is changed, you should also  | ||||
|  - If a new feature is added or an existing user-facing feature is changed, you should also | ||||
|    update the [docs](https://github.com/esphome/esphome-docs). See [contributing to esphome-docs](https://esphome.io/guides/contributing.html#contributing-to-esphomedocs) | ||||
|    for more information. | ||||
|  - Please also update the tests in the `tests/` folder. You can do so by just adding a line in one of the YAML files | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| include LICENSE | ||||
| include README.md | ||||
| include requirements.txt | ||||
| include esphome/dashboard/templates/*.html | ||||
| recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE | ||||
| recursive-include esphome *.cpp *.h *.tcc | ||||
| recursive-include esphome *.cpp *.h *.tcc *.c | ||||
| recursive-include esphome *.py.script | ||||
| recursive-include esphome LICENSE.txt | ||||
|   | ||||
| @@ -5,29 +5,32 @@ | ||||
| # One of "docker", "hassio" | ||||
| ARG BASEIMGTYPE=docker | ||||
|  | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 | ||||
| FROM debian:bullseye-20220125-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20220125-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20220125-slim AS base-docker-armv7 | ||||
| # https://github.com/hassio-addons/addon-debian-base/releases | ||||
| FROM ghcr.io/hassio-addons/debian-base:6.2.3 AS base-hassio | ||||
| # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye | ||||
| FROM debian:bullseye-20230208-slim AS base-docker | ||||
|  | ||||
| # Use TARGETARCH/TARGETVARIANT defined by docker | ||||
| # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope | ||||
| FROM base-${BASEIMGTYPE}-${TARGETARCH}${TARGETVARIANT} AS base | ||||
| FROM base-${BASEIMGTYPE} AS base | ||||
|  | ||||
| ARG TARGETARCH | ||||
| ARG TARGETVARIANT | ||||
|  | ||||
| RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         python3=3.9.2-3 \ | ||||
|         python3-pip=20.3.4-4 \ | ||||
|         python3-pip=20.3.4-4+deb11u1 \ | ||||
|         python3-setuptools=52.0.0-4 \ | ||||
|         python3-pil=8.1.2+dfsg-0.3+deb11u1 \ | ||||
|         python3-cryptography=3.3.2-1 \ | ||||
|         python3-venv=3.9.2-3 \ | ||||
|         iputils-ping=3:20210202-1 \ | ||||
|         git=1:2.30.2-1 \ | ||||
|         curl=7.74.0-1.3+deb11u1 \ | ||||
|         git=1:2.30.2-1+deb11u2 \ | ||||
|         curl=7.74.0-1.3+deb11u7 \ | ||||
|         openssh-client=1:8.4p1-5+deb11u1 \ | ||||
|         libcairo2=1.16.0-5 \ | ||||
|         python3-cffi=1.14.5-1 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -39,29 +42,35 @@ ENV \ | ||||
|   # Store globally installed pio libs in /piolibs | ||||
|   PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
|  | ||||
| # Support legacy binaries on Debian multiarch system. There is no "correct" way | ||||
| # to do this, other than using properly built toolchains... | ||||
| # See: https://unix.stackexchange.com/questions/553743/correct-way-to-add-lib-ld-linux-so-3-in-debian | ||||
| RUN \ | ||||
|     if [ "$TARGETARCH$TARGETVARIANT" = "armv7" ]; then \ | ||||
|         ln -s /lib/arm-linux-gnueabihf/ld-linux.so.3 /lib/ld-linux.so.3; \ | ||||
|     fi | ||||
|  | ||||
| RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     pip3 install --no-cache-dir \ | ||||
|         wheel==0.37.1 \ | ||||
|         platformio==5.2.4 \ | ||||
|         platformio==6.1.7 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_libraries_interval 1000000 \ | ||||
|     && platformio settings set check_platformio_interval 1000000 \ | ||||
|     && platformio settings set check_platforms_interval 1000000 \ | ||||
|     && mkdir -p /piolibs | ||||
|  | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt script/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini --libraries | ||||
|  | ||||
|  | ||||
| # ======================= docker-type image ======================= | ||||
| FROM base AS docker | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome | ||||
| @@ -93,7 +102,7 @@ RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         nginx=1.18.0-6.1 \ | ||||
|         nginx-light=1.18.0-6.1+deb11u3 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -102,13 +111,7 @@ RUN \ | ||||
| ARG BUILD_VERSION=dev | ||||
|  | ||||
| # Copy root filesystem | ||||
| COPY docker/hassio-rootfs/ / | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
| COPY docker/ha-addon-rootfs/ / | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| @@ -135,11 +138,11 @@ RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         clang-format-11=1:11.0.1-2 \ | ||||
|         clang-format-13=1:13.0.1-6~deb11u1 \ | ||||
|         clang-tidy-11=1:11.0.1-2 \ | ||||
|         patch=2.7.6-7 \ | ||||
|         software-properties-common=0.96.20.2-2.1 \ | ||||
|         nano=5.4-2 \ | ||||
|         nano=5.4-2+deb11u2 \ | ||||
|         build-essential=12.9 \ | ||||
|         python3-dev=3.9.2-3 \ | ||||
|     && rm -rf \ | ||||
| @@ -147,10 +150,8 @@ RUN \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
| COPY requirements_test.txt / | ||||
| RUN pip3 install --no-cache-dir -r /requirements_test.txt | ||||
|  | ||||
| VOLUME ["/esphome"] | ||||
| WORKDIR /esphome | ||||
|   | ||||
| @@ -8,32 +8,49 @@ import re | ||||
| import sys | ||||
|  | ||||
|  | ||||
| CHANNEL_DEV = 'dev' | ||||
| CHANNEL_BETA = 'beta' | ||||
| CHANNEL_RELEASE = 'release' | ||||
| CHANNEL_DEV = "dev" | ||||
| CHANNEL_BETA = "beta" | ||||
| CHANNEL_RELEASE = "release" | ||||
| CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] | ||||
|  | ||||
| ARCH_AMD64 = 'amd64' | ||||
| ARCH_ARMV7 = 'armv7' | ||||
| ARCH_AARCH64 = 'aarch64' | ||||
| ARCH_AMD64 = "amd64" | ||||
| ARCH_ARMV7 = "armv7" | ||||
| ARCH_AARCH64 = "aarch64" | ||||
| ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] | ||||
|  | ||||
| TYPE_DOCKER = 'docker' | ||||
| TYPE_HA_ADDON = 'ha-addon' | ||||
| TYPE_LINT = 'lint' | ||||
| TYPE_DOCKER = "docker" | ||||
| TYPE_HA_ADDON = "ha-addon" | ||||
| TYPE_LINT = "lint" | ||||
| TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | ||||
|  | ||||
|  | ||||
| parser = argparse.ArgumentParser() | ||||
| parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") | ||||
| parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") | ||||
| parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") | ||||
| parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") | ||||
| subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) | ||||
| parser.add_argument( | ||||
|     "--tag", | ||||
|     type=str, | ||||
|     required=True, | ||||
|     help="The main docker tag to push to. If a version number also adds latest and/or beta tag", | ||||
| ) | ||||
| parser.add_argument( | ||||
|     "--arch", choices=ARCHS, required=False, help="The architecture to build for" | ||||
| ) | ||||
| parser.add_argument( | ||||
|     "--build-type", choices=TYPES, required=True, help="The type of build to run" | ||||
| ) | ||||
| parser.add_argument( | ||||
|     "--dry-run", action="store_true", help="Don't run any commands, just print them" | ||||
| ) | ||||
| subparsers = parser.add_subparsers( | ||||
|     help="Action to perform", dest="command", required=True | ||||
| ) | ||||
| build_parser = subparsers.add_parser("build", help="Build the image") | ||||
| build_parser.add_argument("--push", help="Also push the images", action="store_true") | ||||
| build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") | ||||
| manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") | ||||
| build_parser.add_argument( | ||||
|     "--load", help="Load the docker image locally", action="store_true" | ||||
| ) | ||||
| manifest_parser = subparsers.add_parser( | ||||
|     "manifest", help="Create a manifest from already pushed images" | ||||
| ) | ||||
|  | ||||
|  | ||||
| @dataclass(frozen=True) | ||||
| @@ -49,7 +66,7 @@ class DockerParams: | ||||
|         prefix = { | ||||
|             TYPE_DOCKER: "esphome/esphome", | ||||
|             TYPE_HA_ADDON: "esphome/esphome-hassio", | ||||
|             TYPE_LINT: "esphome/esphome-lint" | ||||
|             TYPE_LINT: "esphome/esphome-lint", | ||||
|         }[build_type] | ||||
|         build_to = f"{prefix}-{arch}" | ||||
|         baseimgtype = { | ||||
| @@ -88,10 +105,12 @@ def main(): | ||||
|                 sys.exit(1) | ||||
|  | ||||
|     # detect channel from tag | ||||
|     match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag) | ||||
|     match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) | ||||
|     major_minor_version = None | ||||
|     if match is None: | ||||
|         channel = CHANNEL_DEV | ||||
|     elif match.group(1) is None: | ||||
|     elif match.group(2) is None: | ||||
|         major_minor_version = match.group(1) | ||||
|         channel = CHANNEL_RELEASE | ||||
|     else: | ||||
|         channel = CHANNEL_BETA | ||||
| @@ -106,6 +125,11 @@ def main(): | ||||
|         tags_to_push.append("beta") | ||||
|         tags_to_push.append("latest") | ||||
|  | ||||
|         # Compatibility with HA tags | ||||
|         if major_minor_version: | ||||
|             tags_to_push.append("stable") | ||||
|             tags_to_push.append(major_minor_version) | ||||
|  | ||||
|     if args.command == "build": | ||||
|         # 1. pull cache image | ||||
|         params = DockerParams.for_type_arch(args.build_type, args.arch) | ||||
| @@ -121,13 +145,21 @@ def main(): | ||||
|  | ||||
|         # 3. build | ||||
|         cmd = [ | ||||
|             "docker", "buildx", "build", | ||||
|             "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", | ||||
|             "--build-arg", f"BUILD_VERSION={args.tag}", | ||||
|             "--cache-from", f"type=registry,ref={cache_img}", | ||||
|             "--file", "docker/Dockerfile", | ||||
|             "--platform", params.platform, | ||||
|             "--target", params.target, | ||||
|             "docker", | ||||
|             "buildx", | ||||
|             "build", | ||||
|             "--build-arg", | ||||
|             f"BASEIMGTYPE={params.baseimgtype}", | ||||
|             "--build-arg", | ||||
|             f"BUILD_VERSION={args.tag}", | ||||
|             "--cache-from", | ||||
|             f"type=registry,ref={cache_img}", | ||||
|             "--file", | ||||
|             "docker/Dockerfile", | ||||
|             "--platform", | ||||
|             params.platform, | ||||
|             "--target", | ||||
|             params.target, | ||||
|         ] | ||||
|         for img in imgs: | ||||
|             cmd += ["--tag", img] | ||||
| @@ -153,9 +185,7 @@ def main(): | ||||
|             run_command(*cmd) | ||||
|         # 2. Push manifests | ||||
|         for target in targets: | ||||
|             run_command( | ||||
|                 "docker", "manifest", "push", target | ||||
|             ) | ||||
|             run_command("docker", "manifest", "push", target) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|   | ||||
							
								
								
									
										68
									
								
								docker/generate_tags.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										68
									
								
								docker/generate_tags.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #!/usr/bin/env python3 | ||||
| import re | ||||
| import os | ||||
| import argparse | ||||
| import json | ||||
|  | ||||
| CHANNEL_DEV = "dev" | ||||
| CHANNEL_BETA = "beta" | ||||
| CHANNEL_RELEASE = "release" | ||||
|  | ||||
| parser = argparse.ArgumentParser() | ||||
| parser.add_argument( | ||||
|     "--tag", | ||||
|     type=str, | ||||
|     required=True, | ||||
|     help="The main docker tag to push to. If a version number also adds latest and/or beta tag", | ||||
| ) | ||||
| parser.add_argument( | ||||
|     "--suffix", | ||||
|     type=str, | ||||
|     required=True, | ||||
|     help="The suffix of the tag.", | ||||
| ) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     # detect channel from tag | ||||
|     match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) | ||||
|     major_minor_version = None | ||||
|     if match is None: | ||||
|         channel = CHANNEL_DEV | ||||
|     elif match.group(2) is None: | ||||
|         major_minor_version = match.group(1) | ||||
|         channel = CHANNEL_RELEASE | ||||
|     else: | ||||
|         channel = CHANNEL_BETA | ||||
|  | ||||
|     tags_to_push = [args.tag] | ||||
|     if channel == CHANNEL_DEV: | ||||
|         tags_to_push.append("dev") | ||||
|     elif channel == CHANNEL_BETA: | ||||
|         tags_to_push.append("beta") | ||||
|     elif channel == CHANNEL_RELEASE: | ||||
|         # Additionally push to beta | ||||
|         tags_to_push.append("beta") | ||||
|         tags_to_push.append("latest") | ||||
|  | ||||
|         if major_minor_version: | ||||
|             tags_to_push.append("stable") | ||||
|             tags_to_push.append(major_minor_version) | ||||
|  | ||||
|     suffix = f"-{args.suffix}" if args.suffix else "" | ||||
|  | ||||
|     with open(os.environ["GITHUB_OUTPUT"], "w") as f: | ||||
|         print(f"channel={channel}", file=f) | ||||
|         print(f"image=esphome/esphome{suffix}", file=f) | ||||
|         full_tags = [] | ||||
|  | ||||
|         for tag in tags_to_push: | ||||
|             full_tags += [f"ghcr.io/esphome/esphome{suffix}:{tag}"] | ||||
|             full_tags += [f"esphome/esphome{suffix}:{tag}"] | ||||
|         print(f"tags={','.join(full_tags)}", file=f) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
| @@ -1,9 +1,9 @@ | ||||
| proxy_http_version 1.1; | ||||
| proxy_ignore_client_abort off; | ||||
| proxy_read_timeout 86400s; | ||||
| proxy_redirect off; | ||||
| proxy_send_timeout 86400s; | ||||
| proxy_max_temp_file_size 0; | ||||
| proxy_http_version          1.1; | ||||
| proxy_ignore_client_abort   off; | ||||
| proxy_read_timeout          86400s; | ||||
| proxy_redirect              off; | ||||
| proxy_send_timeout          86400s; | ||||
| proxy_max_temp_file_size    0; | ||||
| 
 | ||||
| proxy_set_header Accept-Encoding ""; | ||||
| proxy_set_header Connection $connection_upgrade; | ||||
| @@ -1,5 +1,7 @@ | ||||
| root /dev/null; | ||||
| server_name $hostname; | ||||
| root            /dev/null; | ||||
| server_name     $hostname; | ||||
| 
 | ||||
| client_max_body_size 512m; | ||||
| 
 | ||||
| add_header X-Content-Type-Options nosniff; | ||||
| add_header X-XSS-Protection "1; mode=block"; | ||||
| @@ -0,0 +1,8 @@ | ||||
| ssl_protocols TLSv1.2 TLSv1.3; | ||||
| ssl_prefer_server_ciphers off; | ||||
| ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; | ||||
| ssl_session_timeout  10m; | ||||
| ssl_session_cache shared:SSL:10m; | ||||
| ssl_session_tickets off; | ||||
| ssl_stapling on; | ||||
| ssl_stapling_verify on; | ||||
							
								
								
									
										3
									
								
								docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								docker/ha-addon-rootfs/etc/nginx/includes/upstream.conf
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| upstream esphome { | ||||
|     server unix:/var/run/esphome.sock; | ||||
| } | ||||
| @@ -2,7 +2,6 @@ daemon off; | ||||
| user root; | ||||
| pid /var/run/nginx.pid; | ||||
| worker_processes 1; | ||||
| # Hass.io addon log | ||||
| error_log /proc/1/fd/1 error; | ||||
| events { | ||||
|     worker_connections 1024; | ||||
| @@ -10,24 +9,22 @@ events { | ||||
| 
 | ||||
| http { | ||||
|     include /etc/nginx/includes/mime.types; | ||||
|     access_log stdout; | ||||
|     default_type application/octet-stream; | ||||
|     gzip on; | ||||
|     keepalive_timeout 65; | ||||
|     sendfile on; | ||||
|     server_tokens off; | ||||
| 
 | ||||
|     access_log              off; | ||||
|     default_type            application/octet-stream; | ||||
|     gzip                    on; | ||||
|     keepalive_timeout       65; | ||||
|     sendfile                on; | ||||
|     server_tokens           off; | ||||
| 
 | ||||
|     tcp_nodelay             on; | ||||
|     tcp_nopush              on; | ||||
| 
 | ||||
|     map $http_upgrade $connection_upgrade { | ||||
|         default upgrade; | ||||
|         ''      close; | ||||
|     } | ||||
| 
 | ||||
|     # Use Hass.io supervisor as resolver | ||||
|     resolver 172.30.32.2; | ||||
| 
 | ||||
|     upstream esphome { | ||||
|         server unix:/var/run/esphome.sock; | ||||
|     } | ||||
| 
 | ||||
|     include /etc/nginx/includes/upstream.conf; | ||||
|     include /etc/nginx/servers/*.conf; | ||||
| } | ||||
							
								
								
									
										1
									
								
								docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docker/ha-addon-rootfs/etc/nginx/servers/.gitkeep
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| Without requirements or design, programming is the art of adding bugs to an empty text file. (Louis Srygley) | ||||
| @@ -1,20 +1,26 @@ | ||||
| server { | ||||
|     listen %%port%% default_server ssl http2; | ||||
|     {{ if not .ssl }} | ||||
|     listen 6052 default_server; | ||||
|     {{ else }} | ||||
|     listen 6052 default_server ssl http2; | ||||
|     {{ end }} | ||||
| 
 | ||||
|     include /etc/nginx/includes/server_params.conf; | ||||
|     include /etc/nginx/includes/proxy_params.conf; | ||||
| 
 | ||||
|     {{ if .ssl }} | ||||
|     include /etc/nginx/includes/ssl_params.conf; | ||||
| 
 | ||||
|     ssl on; | ||||
|     ssl_certificate /ssl/%%certfile%%; | ||||
|     ssl_certificate_key /ssl/%%keyfile%%; | ||||
| 
 | ||||
|     # Clear Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress ""; | ||||
|     ssl_certificate /ssl/{{ .certfile }}; | ||||
|     ssl_certificate_key /ssl/{{ .keyfile }}; | ||||
| 
 | ||||
|     # Redirect http requests to https on the same port. | ||||
|     # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ | ||||
|     error_page 497 https://$http_host$request_uri; | ||||
|     {{ end }} | ||||
| 
 | ||||
|     # Clear Home Assistant Ingress header | ||||
|     proxy_set_header X-HA-Ingress ""; | ||||
| 
 | ||||
|     location / { | ||||
|         proxy_pass http://esphome; | ||||
| @@ -1,14 +1,16 @@ | ||||
| server { | ||||
|     listen %%interface%%:%%port%% default_server; | ||||
|     listen 127.0.0.1:{{ .port }} default_server; | ||||
|     listen {{ .interface }}:{{ .port }} default_server; | ||||
| 
 | ||||
|     include /etc/nginx/includes/server_params.conf; | ||||
|     include /etc/nginx/includes/proxy_params.conf; | ||||
|     # Set Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress "YES"; | ||||
| 
 | ||||
|     # Set Home Assistant Ingress header | ||||
|     proxy_set_header X-HA-Ingress "YES"; | ||||
| 
 | ||||
|     location / { | ||||
|         # Only allow from Hass.io supervisor | ||||
|         allow   172.30.32.2; | ||||
|         allow   127.0.0.1; | ||||
|         deny    all; | ||||
| 
 | ||||
|         proxy_pass http://esphome; | ||||
							
								
								
									
										32
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										32
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/discovery/run
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| #!/command/with-contenv bashio | ||||
| # shellcheck shell=bash | ||||
| # ============================================================================== | ||||
| # Home Assistant Add-on: ESPHome | ||||
| # Sends discovery information to Home Assistant. | ||||
| # ============================================================================== | ||||
| declare config | ||||
| declare port | ||||
|  | ||||
| # We only disable it when disabled explicitly | ||||
| if bashio::config.false 'home_assistant_dashboard_integration'; | ||||
| then | ||||
|     bashio::log.info "Home Assistant discovery is disabled for this add-on." | ||||
|     bashio::exit.ok | ||||
| fi | ||||
|  | ||||
| port=$(bashio::addon.ingress_port) | ||||
|  | ||||
| # Wait for NGINX to become available | ||||
| bashio::net.wait_for "${port}" "127.0.0.1" 300 | ||||
|  | ||||
| config=$(\ | ||||
|     bashio::var.json \ | ||||
|         host "127.0.0.1" \ | ||||
|         port "^${port}" \ | ||||
| ) | ||||
|  | ||||
| if bashio::discovery "esphome" "${config}" > /dev/null; then | ||||
|     bashio::log.info "Successfully send discovery information to Home Assistant." | ||||
| else | ||||
|     bashio::log.error "Discovery message to Home Assistant failed!" | ||||
| fi | ||||
| @@ -0,0 +1 @@ | ||||
| oneshot | ||||
| @@ -0,0 +1 @@ | ||||
| /etc/s6-overlay/s6-rc.d/discovery/run | ||||
							
								
								
									
										26
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										26
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/esphome/finish
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| #!/command/with-contenv bashio | ||||
| # shellcheck shell=bash | ||||
| # ============================================================================== | ||||
| # Home Assistant Community Add-on: ESPHome | ||||
| # Take down the S6 supervision tree when ESPHome dashboard fails | ||||
| # ============================================================================== | ||||
| declare exit_code | ||||
| readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode) | ||||
| readonly exit_code_service="${1}" | ||||
| readonly exit_code_signal="${2}" | ||||
|  | ||||
| bashio::log.info \ | ||||
|   "Service ESPHome dashboard exited with code ${exit_code_service}" \ | ||||
|   "(by signal ${exit_code_signal})" | ||||
|  | ||||
| if [[ "${exit_code_service}" -eq 256 ]]; then | ||||
|   if [[ "${exit_code_container}" -eq 0 ]]; then | ||||
|     echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode | ||||
|   fi | ||||
|   [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt | ||||
| elif [[ "${exit_code_service}" -ne 0 ]]; then | ||||
|   if [[ "${exit_code_container}" -eq 0 ]]; then | ||||
|     echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode | ||||
|   fi | ||||
|   exec /run/s6/basedir/bin/halt | ||||
| fi | ||||
| @@ -1,10 +1,19 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| #!/command/with-contenv bashio | ||||
| # shellcheck shell=bash | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Runs the ESPHome dashboard | ||||
| # ============================================================================== | ||||
| readonly pio_cache_base=/data/cache/platformio | ||||
| 
 | ||||
| export ESPHOME_IS_HASSIO=true | ||||
| export ESPHOME_IS_HA_ADDON=true | ||||
| export PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
| 
 | ||||
| # we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` | ||||
| # setting `core_dir` would therefore prevent pio from accessing | ||||
| export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" | ||||
| export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" | ||||
| export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" | ||||
| 
 | ||||
| if bashio::config.true 'leave_front_door_open'; then | ||||
|     export DISABLE_HA_AUTHENTICATION=true | ||||
| @@ -22,14 +31,15 @@ if bashio::config.has_value 'relative_url'; then | ||||
|     export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') | ||||
| fi | ||||
| 
 | ||||
| pio_cache_base=/data/cache/platformio | ||||
| # we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` | ||||
| # setting `core_dir` would therefore prevent pio from accessing | ||||
| export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" | ||||
| export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" | ||||
| export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" | ||||
| if bashio::config.has_value 'default_compile_process_limit'; then | ||||
|     export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit') | ||||
| else | ||||
|     if grep -q 'Raspberry Pi 3' /proc/cpuinfo; then | ||||
|         export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1; | ||||
|     fi | ||||
| fi | ||||
| 
 | ||||
| export PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
| mkdir -p "${pio_cache_base}" | ||||
| 
 | ||||
| bashio::log.info "Starting ESPHome dashboard..." | ||||
| exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio | ||||
| exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --ha-addon | ||||
| @@ -0,0 +1 @@ | ||||
| longrun | ||||
							
								
								
									
										27
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										27
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| #!/command/with-contenv bashio | ||||
| # shellcheck shell=bash | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Configures NGINX for use with ESPHome | ||||
| # ============================================================================== | ||||
| mkdir -p /var/log/nginx | ||||
|  | ||||
| # Generate Ingress configuration | ||||
| bashio::var.json \ | ||||
|     interface "$(bashio::addon.ip_address)" \ | ||||
|     port "^$(bashio::addon.ingress_port)" \ | ||||
|     | tempio \ | ||||
|         -template /etc/nginx/templates/ingress.gtpl \ | ||||
|         -out /etc/nginx/servers/ingress.conf | ||||
|  | ||||
| # Generate direct access configuration, if enabled. | ||||
| if bashio::var.has_value "$(bashio::addon.port 6052)"; then | ||||
|     bashio::config.require.ssl | ||||
|     bashio::var.json \ | ||||
|         certfile "$(bashio::config 'certfile')" \ | ||||
|         keyfile "$(bashio::config 'keyfile')" \ | ||||
|         ssl "^$(bashio::config 'ssl')" \ | ||||
|         | tempio \ | ||||
|             -template /etc/nginx/templates/direct.gtpl \ | ||||
|             -out /etc/nginx/servers/direct.conf | ||||
| fi | ||||
| @@ -0,0 +1 @@ | ||||
| oneshot | ||||
| @@ -0,0 +1 @@ | ||||
| /etc/s6-overlay/s6-rc.d/init-nginx/run | ||||
							
								
								
									
										25
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/finish
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										25
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/finish
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,25 @@ | ||||
| #!/command/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Take down the S6 supervision tree when NGINX fails | ||||
| # ============================================================================== | ||||
| declare exit_code | ||||
| readonly exit_code_container=$(</run/s6-linux-init-container-results/exitcode) | ||||
| readonly exit_code_service="${1}" | ||||
| readonly exit_code_signal="${2}" | ||||
|  | ||||
| bashio::log.info \ | ||||
|   "Service NGINX exited with code ${exit_code_service}" \ | ||||
|   "(by signal ${exit_code_signal})" | ||||
|  | ||||
| if [[ "${exit_code_service}" -eq 256 ]]; then | ||||
|   if [[ "${exit_code_container}" -eq 0 ]]; then | ||||
|     echo $((128 + $exit_code_signal)) > /run/s6-linux-init-container-results/exitcode | ||||
|   fi | ||||
|   [[ "${exit_code_signal}" -eq 15 ]] && exec /run/s6/basedir/bin/halt | ||||
| elif [[ "${exit_code_service}" -ne 0 ]]; then | ||||
|   if [[ "${exit_code_container}" -eq 0 ]]; then | ||||
|     echo "${exit_code_service}" > /run/s6-linux-init-container-results/exitcode | ||||
|   fi | ||||
|   exec /run/s6/basedir/bin/halt | ||||
| fi | ||||
| @@ -1,10 +1,11 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| #!/command/with-contenv bashio | ||||
| # shellcheck shell=bash | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Runs the NGINX proxy | ||||
| # ============================================================================== | ||||
| 
 | ||||
| bashio::log.info "Waiting for dashboard to come up..." | ||||
| bashio::log.info "Waiting for ESPHome dashboard to come up..." | ||||
| 
 | ||||
| while [[ ! -S /var/run/esphome.sock ]]; do | ||||
|   sleep 0.5 | ||||
							
								
								
									
										1
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								docker/ha-addon-rootfs/etc/s6-overlay/s6-rc.d/nginx/type
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| longrun | ||||
| @@ -1,41 +0,0 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # This files check if all user configuration requirements are met | ||||
| # ============================================================================== | ||||
|  | ||||
| # Check SSL requirements, if enabled | ||||
| if bashio::config.true 'ssl'; then | ||||
|     if ! bashio::config.has_value 'certfile'; then | ||||
|         bashio::fatal 'SSL is enabled, but no certfile was specified.' | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
|  | ||||
|     if ! bashio::config.has_value 'keyfile'; then | ||||
|         bashio::fatal 'SSL is enabled, but no keyfile was specified' | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
|  | ||||
|  | ||||
|     certfile="/ssl/$(bashio::config 'certfile')" | ||||
|     keyfile="/ssl/$(bashio::config 'keyfile')" | ||||
|  | ||||
|     if ! bashio::fs.file_exists "${certfile}"; then | ||||
|         if ! bashio::fs.file_exists "${keyfile}"; then | ||||
|             # Both files are missing, let's print a friendlier error message | ||||
|             bashio::log.fatal 'You enabled encrypted connections using the "ssl": true option.' | ||||
|             bashio::log.fatal "However, the SSL files '${certfile}' and '${keyfile}'" | ||||
|             bashio::log.fatal "were not found. If you're using Hass.io on your local network and don't want" | ||||
|             bashio::log.fatal 'to encrypt connections to the ESPHome dashboard, you can manually disable' | ||||
|             bashio::log.fatal 'SSL by setting "ssl" to false."' | ||||
|             bashio::exit.nok | ||||
|         fi | ||||
|         bashio::log.fatal "The configured certfile '${certfile}' was not found." | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
|  | ||||
|     if ! bashio::fs.file_exists "/ssl/$(bashio::config 'keyfile')"; then | ||||
|         bashio::log.fatal "The configured keyfile '${keyfile}' was not found." | ||||
|         bashio::exit.nok | ||||
|     fi | ||||
| fi | ||||
| @@ -1,34 +0,0 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Configures NGINX for use with ESPHome | ||||
| # ============================================================================== | ||||
|  | ||||
| declare certfile | ||||
| declare keyfile | ||||
| declare direct_port | ||||
| declare ingress_interface | ||||
| declare ingress_port | ||||
|  | ||||
| mkdir -p /var/log/nginx | ||||
|  | ||||
| direct_port=$(bashio::addon.port 6052) | ||||
| if bashio::var.has_value "${direct_port}"; then | ||||
|     if bashio::config.true 'ssl'; then | ||||
|         certfile=$(bashio::config 'certfile') | ||||
|         keyfile=$(bashio::config 'keyfile') | ||||
|  | ||||
|         mv /etc/nginx/servers/direct-ssl.disabled /etc/nginx/servers/direct.conf | ||||
|         sed -i "s/%%certfile%%/${certfile}/g" /etc/nginx/servers/direct.conf | ||||
|         sed -i "s/%%keyfile%%/${keyfile}/g" /etc/nginx/servers/direct.conf | ||||
|     else | ||||
|         mv /etc/nginx/servers/direct.disabled /etc/nginx/servers/direct.conf | ||||
|     fi | ||||
|  | ||||
|     sed -i "s/%%port%%/${direct_port}/g" /etc/nginx/servers/direct.conf | ||||
| fi | ||||
|  | ||||
| ingress_port=$(bashio::addon.ingress_port) | ||||
| ingress_interface=$(bashio::addon.ip_address) | ||||
| sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf | ||||
| sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf | ||||
| @@ -1,9 +0,0 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # This files creates all directories used by esphome | ||||
| # ============================================================================== | ||||
|  | ||||
| pio_cache_base=/data/cache/platformio | ||||
|  | ||||
| mkdir -p "${pio_cache_base}" | ||||
| @@ -1,9 +0,0 @@ | ||||
| ssl_protocols TLSv1.2; | ||||
| ssl_prefer_server_ciphers on; | ||||
| ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:DHE-RSA-AES256-SHA; | ||||
| ssl_ecdh_curve secp384r1; | ||||
| ssl_session_timeout  10m; | ||||
| ssl_session_cache shared:SSL:10m; | ||||
| ssl_session_tickets off; | ||||
| ssl_stapling on; | ||||
| ssl_stapling_verify on; | ||||
| @@ -1,12 +0,0 @@ | ||||
| server { | ||||
|     listen %%port%% default_server; | ||||
|  | ||||
|     include /etc/nginx/includes/server_params.conf; | ||||
|     include /etc/nginx/includes/proxy_params.conf; | ||||
|     # Clear Hass.io Ingress header | ||||
|     proxy_set_header X-Hassio-Ingress ""; | ||||
|  | ||||
|     location / { | ||||
|         proxy_pass http://esphome; | ||||
|     } | ||||
| } | ||||
| @@ -1,9 +0,0 @@ | ||||
| #!/usr/bin/execlineb -S0 | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Take down the S6 supervision tree when ESPHome fails | ||||
| # ============================================================================== | ||||
| if -n { s6-test $# -ne 0 } | ||||
| if -n { s6-test ${1} -eq 256 } | ||||
|  | ||||
| s6-svscanctl -t /var/run/s6/services | ||||
| @@ -1,9 +0,0 @@ | ||||
| #!/usr/bin/execlineb -S0 | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # Take down the S6 supervision tree when NGINX fails | ||||
| # ============================================================================== | ||||
| if -n { s6-test $# -ne 0 } | ||||
| if -n { s6-test ${1} -eq 256 } | ||||
|  | ||||
| s6-svscanctl -t /var/run/s6/services | ||||
| @@ -1,30 +0,0 @@ | ||||
| #!/usr/bin/env python3 | ||||
| # This script is used in the docker containers to preinstall | ||||
| # all platformio libraries in the global storage | ||||
|  | ||||
| import configparser | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| config = configparser.ConfigParser(inline_comment_prefixes=(';', )) | ||||
| config.read(sys.argv[1]) | ||||
|  | ||||
| libs = [] | ||||
| # Extract from every lib_deps key in all sections | ||||
| for section in config.sections(): | ||||
|     conf = config[section] | ||||
|     if "lib_deps" not in conf: | ||||
|         continue | ||||
|     for lib_dep in conf["lib_deps"].splitlines(): | ||||
|         if not lib_dep: | ||||
|             # Empty line or comment | ||||
|             continue | ||||
|         if lib_dep.startswith("${"): | ||||
|             # Extending from another section | ||||
|             continue | ||||
|         if "@" not in lib_dep: | ||||
|             # No version pinned, this is an internal lib | ||||
|             continue | ||||
|         libs.append(lib_dep) | ||||
|  | ||||
| subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs]) | ||||
| @@ -2,22 +2,33 @@ import argparse | ||||
| import functools | ||||
| import logging | ||||
| import os | ||||
| import re | ||||
| import sys | ||||
| import time | ||||
| from datetime import datetime | ||||
|  | ||||
| from esphome import const, writer, yaml_util | ||||
| import esphome.codegen as cg | ||||
| from esphome.config import iter_components, read_config, strip_default_ids | ||||
| from esphome.const import ( | ||||
|     ALLOWED_NAME_CHARS, | ||||
|     CONF_BAUD_RATE, | ||||
|     CONF_BROKER, | ||||
|     CONF_DEASSERT_RTS_DTR, | ||||
|     CONF_LOGGER, | ||||
|     CONF_NAME, | ||||
|     CONF_OTA, | ||||
|     CONF_MQTT, | ||||
|     CONF_MDNS, | ||||
|     CONF_DISABLED, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PORT, | ||||
|     CONF_ESPHOME, | ||||
|     CONF_PLATFORMIO_OPTIONS, | ||||
|     CONF_SUBSTITUTIONS, | ||||
|     PLATFORM_ESP32, | ||||
|     PLATFORM_ESP8266, | ||||
|     PLATFORM_RP2040, | ||||
|     SECRETS_FILES, | ||||
| ) | ||||
| from esphome.core import CORE, EsphomeError, coroutine | ||||
| @@ -34,7 +45,7 @@ from esphome.log import color, setup_log, Fore | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| def choose_prompt(options): | ||||
| def choose_prompt(options, purpose: str = None): | ||||
|     if not options: | ||||
|         raise EsphomeError( | ||||
|             "Found no valid options for upload/logging, please make sure relevant " | ||||
| @@ -45,7 +56,9 @@ def choose_prompt(options): | ||||
|     if len(options) == 1: | ||||
|         return options[0][1] | ||||
|  | ||||
|     safe_print("Found multiple options, please choose one:") | ||||
|     safe_print( | ||||
|         f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' | ||||
|     ) | ||||
|     for i, (desc, _) in enumerate(options): | ||||
|         safe_print(f"  [{i+1}] {desc}") | ||||
|  | ||||
| @@ -64,7 +77,9 @@ def choose_prompt(options): | ||||
|     return options[opt - 1][1] | ||||
|  | ||||
|  | ||||
| def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): | ||||
| def choose_upload_log_host( | ||||
|     default, check_default, show_ota, show_mqtt, show_api, purpose: str = None | ||||
| ): | ||||
|     options = [] | ||||
|     for port in get_serial_ports(): | ||||
|         options.append((f"{port.path} ({port.description})", port.path)) | ||||
| @@ -72,7 +87,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | ||||
|         options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||
|         if default == "OTA": | ||||
|             return CORE.address | ||||
|     if show_mqtt and "mqtt" in CORE.config: | ||||
|     if show_mqtt and CONF_MQTT in CORE.config: | ||||
|         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) | ||||
|         if default == "OTA": | ||||
|             return "MQTT" | ||||
| @@ -80,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | ||||
|         return default | ||||
|     if check_default is not None and check_default in [opt[1] for opt in options]: | ||||
|         return check_default | ||||
|     return choose_prompt(options) | ||||
|     return choose_prompt(options, purpose=purpose) | ||||
|  | ||||
|  | ||||
| def get_port_type(port): | ||||
| @@ -97,11 +112,11 @@ def run_miniterm(config, port): | ||||
|  | ||||
|     if CONF_LOGGER not in config: | ||||
|         _LOGGER.info("Logger is not enabled. Not starting UART logs.") | ||||
|         return | ||||
|         return 1 | ||||
|     baud_rate = config["logger"][CONF_BAUD_RATE] | ||||
|     if baud_rate == 0: | ||||
|         _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") | ||||
|         return | ||||
|         return 1 | ||||
|     _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) | ||||
|  | ||||
|     backtrace_state = False | ||||
| @@ -115,25 +130,36 @@ def run_miniterm(config, port): | ||||
|         ser.dtr = False | ||||
|         ser.rts = False | ||||
|  | ||||
|     with ser: | ||||
|         while True: | ||||
|             try: | ||||
|                 raw = ser.readline() | ||||
|             except serial.SerialException: | ||||
|                 _LOGGER.error("Serial port closed!") | ||||
|                 return | ||||
|             line = ( | ||||
|                 raw.replace(b"\r", b"") | ||||
|                 .replace(b"\n", b"") | ||||
|                 .decode("utf8", "backslashreplace") | ||||
|             ) | ||||
|             time = datetime.now().time().strftime("[%H:%M:%S]") | ||||
|             message = time + line | ||||
|             safe_print(message) | ||||
|     tries = 0 | ||||
|     while tries < 5: | ||||
|         try: | ||||
|             with ser: | ||||
|                 while True: | ||||
|                     try: | ||||
|                         raw = ser.readline() | ||||
|                     except serial.SerialException: | ||||
|                         _LOGGER.error("Serial port closed!") | ||||
|                         return 0 | ||||
|                     line = ( | ||||
|                         raw.replace(b"\r", b"") | ||||
|                         .replace(b"\n", b"") | ||||
|                         .decode("utf8", "backslashreplace") | ||||
|                     ) | ||||
|                     time_str = datetime.now().time().strftime("[%H:%M:%S]") | ||||
|                     message = time_str + line | ||||
|                     safe_print(message) | ||||
|  | ||||
|             backtrace_state = platformio_api.process_stacktrace( | ||||
|                 config, line, backtrace_state=backtrace_state | ||||
|             ) | ||||
|                     backtrace_state = platformio_api.process_stacktrace( | ||||
|                         config, line, backtrace_state=backtrace_state | ||||
|                     ) | ||||
|         except serial.SerialException: | ||||
|             tries += 1 | ||||
|             time.sleep(1) | ||||
|     if tries >= 5: | ||||
|         _LOGGER.error("Could not connect to serial port %s", port) | ||||
|         return 1 | ||||
|  | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def wrap_to_code(name, comp): | ||||
| @@ -237,8 +263,7 @@ def upload_using_esptool(config, port): | ||||
|         if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: | ||||
|             import esptool | ||||
|  | ||||
|             # pylint: disable=protected-access | ||||
|             return run_external_command(esptool._main, *cmd) | ||||
|             return run_external_command(esptool.main, *cmd)  # pylint: disable=no-member | ||||
|  | ||||
|         return run_external_process(*cmd) | ||||
|  | ||||
| @@ -254,11 +279,21 @@ def upload_using_esptool(config, port): | ||||
|  | ||||
|  | ||||
| def upload_program(config, args, host): | ||||
|     # if upload is to a serial port use platformio, otherwise assume ota | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         return upload_using_esptool(config, host) | ||||
|         if CORE.target_platform in (PLATFORM_ESP32, PLATFORM_ESP8266): | ||||
|             return upload_using_esptool(config, host) | ||||
|  | ||||
|     from esphome import espota2 | ||||
|         if CORE.target_platform in (PLATFORM_RP2040): | ||||
|             from esphome import platformio_api | ||||
|  | ||||
|             upload_args = ["-t", "upload"] | ||||
|             if args.device is not None: | ||||
|                 upload_args += ["--upload-port", args.device] | ||||
|             return platformio_api.run_platformio_cli_run( | ||||
|                 config, CORE.verbose, *upload_args | ||||
|             ) | ||||
|  | ||||
|         return 1  # Unknown target platform | ||||
|  | ||||
|     if CONF_OTA not in config: | ||||
|         raise EsphomeError( | ||||
| @@ -266,9 +301,24 @@ def upload_program(config, args, host): | ||||
|             "component" | ||||
|         ) | ||||
|  | ||||
|     from esphome import espota2 | ||||
|  | ||||
|     ota_conf = config[CONF_OTA] | ||||
|     remote_port = ota_conf[CONF_PORT] | ||||
|     password = ota_conf.get(CONF_PASSWORD, "") | ||||
|  | ||||
|     if ( | ||||
|         get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] | ||||
|     ) and CONF_MQTT in config: | ||||
|         from esphome import mqtt | ||||
|  | ||||
|         host = mqtt.get_esphome_device_ip( | ||||
|             config, args.username, args.password, args.client_id | ||||
|         ) | ||||
|  | ||||
|     if getattr(args, "file", None) is not None: | ||||
|         return espota2.run_ota(host, remote_port, password, args.file) | ||||
|  | ||||
|     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) | ||||
|  | ||||
|  | ||||
| @@ -276,9 +326,15 @@ def show_logs(config, args, port): | ||||
|     if "logger" not in config: | ||||
|         raise EsphomeError("Logger is not configured!") | ||||
|     if get_port_type(port) == "SERIAL": | ||||
|         run_miniterm(config, port) | ||||
|         return 0 | ||||
|         return run_miniterm(config, port) | ||||
|     if get_port_type(port) == "NETWORK" and "api" in config: | ||||
|         if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: | ||||
|             from esphome import mqtt | ||||
|  | ||||
|             port = mqtt.get_esphome_device_ip( | ||||
|                 config, args.username, args.password, args.client_id | ||||
|             ) | ||||
|  | ||||
|         from esphome.components.api.client import run_logs | ||||
|  | ||||
|         return run_logs(config, port) | ||||
| @@ -310,7 +366,7 @@ def command_config(args, config): | ||||
|     _LOGGER.info("Configuration is valid!") | ||||
|     if not CORE.verbose: | ||||
|         config = strip_default_ids(config) | ||||
|     safe_print(yaml_util.dump(config)) | ||||
|     safe_print(yaml_util.dump(config, args.show_secrets)) | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| @@ -343,6 +399,7 @@ def command_upload(args, config): | ||||
|         show_ota=True, | ||||
|         show_mqtt=False, | ||||
|         show_api=False, | ||||
|         purpose="uploading", | ||||
|     ) | ||||
|     exit_code = upload_program(config, args, port) | ||||
|     if exit_code != 0: | ||||
| @@ -351,6 +408,15 @@ def command_upload(args, config): | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def command_discover(args, config): | ||||
|     if "mqtt" in config: | ||||
|         from esphome import mqtt | ||||
|  | ||||
|         return mqtt.show_discover(config, args.username, args.password, args.client_id) | ||||
|  | ||||
|     raise EsphomeError("No discover method configured (mqtt)") | ||||
|  | ||||
|  | ||||
| def command_logs(args, config): | ||||
|     port = choose_upload_log_host( | ||||
|         default=args.device, | ||||
| @@ -358,6 +424,7 @@ def command_logs(args, config): | ||||
|         show_ota=False, | ||||
|         show_mqtt=True, | ||||
|         show_api=True, | ||||
|         purpose="logging", | ||||
|     ) | ||||
|     return show_logs(config, args, port) | ||||
|  | ||||
| @@ -376,6 +443,7 @@ def command_run(args, config): | ||||
|         show_ota=True, | ||||
|         show_mqtt=False, | ||||
|         show_api=True, | ||||
|         purpose="uploading", | ||||
|     ) | ||||
|     exit_code = upload_program(config, args, port) | ||||
|     if exit_code != 0: | ||||
| @@ -389,6 +457,7 @@ def command_run(args, config): | ||||
|         show_ota=False, | ||||
|         show_mqtt=True, | ||||
|         show_api=True, | ||||
|         purpose="logging", | ||||
|     ) | ||||
|     return show_logs(config, args, port) | ||||
|  | ||||
| @@ -481,6 +550,98 @@ def command_idedata(args, config): | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| def command_rename(args, config): | ||||
|     for c in args.name: | ||||
|         if c not in ALLOWED_NAME_CHARS: | ||||
|             print( | ||||
|                 color( | ||||
|                     Fore.BOLD_RED, | ||||
|                     f"'{c}' is an invalid character for names. Valid characters are: " | ||||
|                     f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)", | ||||
|                 ) | ||||
|             ) | ||||
|             return 1 | ||||
|     # Load existing yaml file | ||||
|     with open(CORE.config_path, mode="r+", encoding="utf-8") as raw_file: | ||||
|         raw_contents = raw_file.read() | ||||
|  | ||||
|     yaml = yaml_util.load_yaml(CORE.config_path) | ||||
|     if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]: | ||||
|         print( | ||||
|             color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.") | ||||
|         ) | ||||
|         return 1 | ||||
|     old_name = yaml[CONF_ESPHOME][CONF_NAME] | ||||
|     match = re.match(r"^\$\{?([a-zA-Z0-9_]+)\}?$", old_name) | ||||
|     if match is None: | ||||
|         new_raw = re.sub( | ||||
|             rf"name:\s+[\"']?{old_name}[\"']?", | ||||
|             f'name: "{args.name}"', | ||||
|             raw_contents, | ||||
|         ) | ||||
|     else: | ||||
|         old_name = yaml[CONF_SUBSTITUTIONS][match.group(1)] | ||||
|         if ( | ||||
|             len( | ||||
|                 re.findall( | ||||
|                     rf"^\s+{match.group(1)}:\s+[\"']?{old_name}[\"']?", | ||||
|                     raw_contents, | ||||
|                     flags=re.MULTILINE, | ||||
|                 ) | ||||
|             ) | ||||
|             > 1 | ||||
|         ): | ||||
|             print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename")) | ||||
|             return 1 | ||||
|  | ||||
|         new_raw = re.sub( | ||||
|             rf"^(\s+{match.group(1)}):\s+[\"']?{old_name}[\"']?", | ||||
|             f'\\1: "{args.name}"', | ||||
|             raw_contents, | ||||
|             flags=re.MULTILINE, | ||||
|         ) | ||||
|  | ||||
|     new_path = os.path.join(CORE.config_dir, args.name + ".yaml") | ||||
|     print( | ||||
|         f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}" | ||||
|     ) | ||||
|     print() | ||||
|  | ||||
|     with open(new_path, mode="w", encoding="utf-8") as new_file: | ||||
|         new_file.write(new_raw) | ||||
|  | ||||
|     rc = run_external_process("esphome", "config", new_path) | ||||
|     if rc != 0: | ||||
|         print(color(Fore.BOLD_RED, "Rename failed. Reverting changes.")) | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     cli_args = [ | ||||
|         "run", | ||||
|         new_path, | ||||
|         "--no-logs", | ||||
|         "--device", | ||||
|         CORE.address, | ||||
|     ] | ||||
|  | ||||
|     if args.dashboard: | ||||
|         cli_args.insert(0, "--dashboard") | ||||
|  | ||||
|     try: | ||||
|         rc = run_external_process("esphome", *cli_args) | ||||
|     except KeyboardInterrupt: | ||||
|         rc = 1 | ||||
|     if rc != 0: | ||||
|         os.remove(new_path) | ||||
|         return 1 | ||||
|  | ||||
|     os.remove(CORE.config_path) | ||||
|  | ||||
|     print(color(Fore.BOLD_GREEN, "SUCCESS")) | ||||
|     print() | ||||
|     return 0 | ||||
|  | ||||
|  | ||||
| PRE_CONFIG_ACTIONS = { | ||||
|     "wizard": command_wizard, | ||||
|     "version": command_version, | ||||
| @@ -499,6 +660,8 @@ POST_CONFIG_ACTIONS = { | ||||
|     "mqtt-fingerprint": command_mqtt_fingerprint, | ||||
|     "clean": command_clean, | ||||
|     "idedata": command_idedata, | ||||
|     "rename": command_rename, | ||||
|     "discover": command_discover, | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -543,6 +706,9 @@ def parse_args(argv): | ||||
|     parser_config.add_argument( | ||||
|         "configuration", help="Your YAML configuration file(s).", nargs="+" | ||||
|     ) | ||||
|     parser_config.add_argument( | ||||
|         "--show-secrets", help="Show secrets in output.", action="store_true" | ||||
|     ) | ||||
|  | ||||
|     parser_compile = subparsers.add_parser( | ||||
|         "compile", help="Read the configuration and compile a program." | ||||
| @@ -566,6 +732,10 @@ def parse_args(argv): | ||||
|         "--device", | ||||
|         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", | ||||
|     ) | ||||
|     parser_upload.add_argument( | ||||
|         "--file", | ||||
|         help="Manually specify the binary file to upload.", | ||||
|     ) | ||||
|  | ||||
|     parser_logs = subparsers.add_parser( | ||||
|         "logs", | ||||
| @@ -580,6 +750,15 @@ def parse_args(argv): | ||||
|         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", | ||||
|     ) | ||||
|  | ||||
|     parser_discover = subparsers.add_parser( | ||||
|         "discover", | ||||
|         help="Validate the configuration and show all discovered devices.", | ||||
|         parents=[mqtt_options], | ||||
|     ) | ||||
|     parser_discover.add_argument( | ||||
|         "configuration", help="Your YAML configuration file.", nargs=1 | ||||
|     ) | ||||
|  | ||||
|     parser_run = subparsers.add_parser( | ||||
|         "run", | ||||
|         help="Validate the configuration, create a binary, upload it, and start logs.", | ||||
| @@ -661,7 +840,7 @@ def parse_args(argv): | ||||
|         "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" | ||||
|     ) | ||||
|     parser_dashboard.add_argument( | ||||
|         "--hassio", help=argparse.SUPPRESS, action="store_true" | ||||
|         "--ha-addon", help=argparse.SUPPRESS, action="store_true" | ||||
|     ) | ||||
|     parser_dashboard.add_argument( | ||||
|         "--socket", help="Make the dashboard serve under a unix socket", type=str | ||||
| @@ -681,6 +860,15 @@ def parse_args(argv): | ||||
|         "configuration", help="Your YAML configuration file(s).", nargs=1 | ||||
|     ) | ||||
|  | ||||
|     parser_rename = subparsers.add_parser( | ||||
|         "rename", | ||||
|         help="Rename a device in YAML, compile the binary and upload it.", | ||||
|     ) | ||||
|     parser_rename.add_argument( | ||||
|         "configuration", help="Your YAML configuration file.", nargs=1 | ||||
|     ) | ||||
|     parser_rename.add_argument("name", help="The new name for the device.", type=str) | ||||
|  | ||||
|     # Keep backward compatibility with the old command line format of | ||||
|     # esphome <config> <command>. | ||||
|     # | ||||
| @@ -778,10 +966,10 @@ def run_esphome(argv): | ||||
|         _LOGGER.warning("Please instead use:") | ||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion)) | ||||
|  | ||||
|     if sys.version_info < (3, 7, 0): | ||||
|     if sys.version_info < (3, 8, 0): | ||||
|         _LOGGER.error( | ||||
|             "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " | ||||
|             "with this Python version. Please reinstall ESPHome with Python 3.7+" | ||||
|             "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " | ||||
|             "with this Python version. Please reinstall ESPHome with Python 3.8+" | ||||
|         ) | ||||
|         return 1 | ||||
|  | ||||
| @@ -792,6 +980,8 @@ def run_esphome(argv): | ||||
|             _LOGGER.error(e, exc_info=args.verbose) | ||||
|             return 1 | ||||
|  | ||||
|     _LOGGER.info("ESPHome %s", const.__version__) | ||||
|  | ||||
|     for conf_path in args.configuration: | ||||
|         if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): | ||||
|             _LOGGER.warning("Skipping secrets file %s", conf_path) | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from esphome.const import ( | ||||
|     CONF_TYPE_ID, | ||||
|     CONF_TIME, | ||||
| ) | ||||
| from esphome.jsonschema import jschema_extractor | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.util import Registry | ||||
|  | ||||
|  | ||||
| @@ -23,11 +23,10 @@ def maybe_simple_id(*validators): | ||||
| def maybe_conf(conf, *validators): | ||||
|     validator = cv.All(*validators) | ||||
|  | ||||
|     @jschema_extractor("maybe") | ||||
|     @schema_extractor("maybe") | ||||
|     def validate(value): | ||||
|         # pylint: disable=comparison-with-callable | ||||
|         if value == jschema_extractor: | ||||
|             return validator | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return (validator, conf) | ||||
|  | ||||
|         if isinstance(value, dict): | ||||
|             return validator(value) | ||||
| @@ -111,11 +110,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|         # This should only happen with invalid configs, but let's have a nice error message. | ||||
|         return [schema(value)] | ||||
|  | ||||
|     @jschema_extractor("automation") | ||||
|     @schema_extractor("automation") | ||||
|     def validator(value): | ||||
|         # hack to get the schema | ||||
|         # pylint: disable=comparison-with-callable | ||||
|         if value == jschema_extractor: | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return schema | ||||
|  | ||||
|         value = validator_(value) | ||||
| @@ -257,26 +254,25 @@ async def repeat_action_to_code(config, action_id, template_arg, args): | ||||
|     var = cg.new_Pvariable(action_id, template_arg) | ||||
|     count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) | ||||
|     cg.add(var.set_count(count_template)) | ||||
|     actions = await build_action_list(config[CONF_THEN], template_arg, args) | ||||
|     actions = await build_action_list( | ||||
|         config[CONF_THEN], | ||||
|         cg.TemplateArguments(cg.uint32, *template_arg.args), | ||||
|         [(cg.uint32, "iteration"), *args], | ||||
|     ) | ||||
|     cg.add(var.add_then(actions)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| def validate_wait_until(value): | ||||
|     schema = cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|             cv.Optional(CONF_TIMEOUT): cv.templatable( | ||||
|                 cv.positive_time_period_milliseconds | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     if isinstance(value, dict) and CONF_CONDITION in value: | ||||
|         return schema(value) | ||||
|     return validate_wait_until({CONF_CONDITION: value}) | ||||
| _validate_wait_until = cv.maybe_simple_value( | ||||
|     { | ||||
|         cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|         cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds), | ||||
|     }, | ||||
|     key=CONF_CONDITION, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @register_action("wait_until", WaitUntilAction, validate_wait_until) | ||||
| @register_action("wait_until", WaitUntilAction, _validate_wait_until) | ||||
| async def wait_until_action_to_code(config, action_id, template_arg, args): | ||||
|     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, conditions) | ||||
|   | ||||
| @@ -22,6 +22,7 @@ from esphome.cpp_generator import (  # noqa | ||||
|     static_const_array, | ||||
|     statement, | ||||
|     variable, | ||||
|     with_local_variable, | ||||
|     new_variable, | ||||
|     Pvariable, | ||||
|     new_Pvariable, | ||||
| @@ -46,6 +47,7 @@ from esphome.cpp_helpers import (  # noqa | ||||
|     build_registry_list, | ||||
|     extract_registry_entry_config, | ||||
|     register_parented, | ||||
|     past_safe_mode, | ||||
| ) | ||||
| from esphome.cpp_types import (  # noqa | ||||
|     global_ns, | ||||
| @@ -62,7 +64,10 @@ from esphome.cpp_types import (  # noqa | ||||
|     uint16, | ||||
|     uint32, | ||||
|     uint64, | ||||
|     int16, | ||||
|     int32, | ||||
|     int64, | ||||
|     size_t, | ||||
|     const_char_ptr, | ||||
|     NAN, | ||||
|     esphome_ns, | ||||
| @@ -81,4 +86,5 @@ from esphome.cpp_types import (  # noqa | ||||
|     InternalGPIOPin, | ||||
|     gpio_Flags, | ||||
|     EntityCategory, | ||||
|     Parented, | ||||
| ) | ||||
|   | ||||
| @@ -46,6 +46,7 @@ void A4988::loop() { | ||||
|     return; | ||||
|  | ||||
|   this->dir_pin_->digital_write(dir == 1); | ||||
|   delayMicroseconds(50); | ||||
|   this->step_pin_->digital_write(true); | ||||
|   delayMicroseconds(5); | ||||
|   this->step_pin_->digital_write(false); | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/absolute_humidity/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/absolute_humidity/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@DAVe3283"] | ||||
							
								
								
									
										182
									
								
								esphome/components/absolute_humidity/absolute_humidity.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								esphome/components/absolute_humidity/absolute_humidity.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | ||||
| #include "esphome/core/log.h" | ||||
| #include "absolute_humidity.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace absolute_humidity { | ||||
|  | ||||
| static const char *const TAG = "absolute_humidity.sensor"; | ||||
|  | ||||
| void AbsoluteHumidityComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up absolute humidity '%s'...", this->get_name().c_str()); | ||||
|  | ||||
|   ESP_LOGD(TAG, "  Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str()); | ||||
|   this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); }); | ||||
|   if (this->temperature_sensor_->has_state()) { | ||||
|     this->temperature_callback_(this->temperature_sensor_->get_state()); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "  Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str()); | ||||
|   this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); }); | ||||
|   if (this->humidity_sensor_->has_state()) { | ||||
|     this->humidity_callback_(this->humidity_sensor_->get_state()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AbsoluteHumidityComponent::dump_config() { | ||||
|   LOG_SENSOR("", "Absolute Humidity", this); | ||||
|  | ||||
|   switch (this->equation_) { | ||||
|     case BUCK: | ||||
|       ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Buck"); | ||||
|       break; | ||||
|     case TETENS: | ||||
|       ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Tetens"); | ||||
|       break; | ||||
|     case WOBUS: | ||||
|       ESP_LOGCONFIG(TAG, "Saturation Vapor Pressure Equation: Wobus"); | ||||
|       break; | ||||
|     default: | ||||
|       ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "Sources"); | ||||
|   ESP_LOGCONFIG(TAG, "  Temperature: '%s'", this->temperature_sensor_->get_name().c_str()); | ||||
|   ESP_LOGCONFIG(TAG, "  Relative Humidity: '%s'", this->humidity_sensor_->get_name().c_str()); | ||||
| } | ||||
|  | ||||
| float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void AbsoluteHumidityComponent::loop() { | ||||
|   if (!this->next_update_) { | ||||
|     return; | ||||
|   } | ||||
|   this->next_update_ = false; | ||||
|  | ||||
|   // Ensure we have source data | ||||
|   const bool no_temperature = std::isnan(this->temperature_); | ||||
|   const bool no_humidity = std::isnan(this->humidity_); | ||||
|   if (no_temperature || no_humidity) { | ||||
|     if (no_temperature) { | ||||
|       ESP_LOGW(TAG, "No valid state from temperature sensor!"); | ||||
|     } | ||||
|     if (no_humidity) { | ||||
|       ESP_LOGW(TAG, "No valid state from temperature sensor!"); | ||||
|     } | ||||
|     ESP_LOGW(TAG, "Unable to calculate absolute humidity."); | ||||
|     this->publish_state(NAN); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Convert to desired units | ||||
|   const float temperature_c = this->temperature_; | ||||
|   const float temperature_k = temperature_c + 273.15; | ||||
|   const float hr = this->humidity_ / 100; | ||||
|  | ||||
|   // Calculate saturation vapor pressure | ||||
|   float es; | ||||
|   switch (this->equation_) { | ||||
|     case BUCK: | ||||
|       es = es_buck(temperature_c); | ||||
|       break; | ||||
|     case TETENS: | ||||
|       es = es_tetens(temperature_c); | ||||
|       break; | ||||
|     case WOBUS: | ||||
|       es = es_wobus(temperature_c); | ||||
|       break; | ||||
|     default: | ||||
|       ESP_LOGE(TAG, "Invalid saturation vapor pressure equation selection!"); | ||||
|       this->publish_state(NAN); | ||||
|       this->status_set_error(); | ||||
|       return; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es); | ||||
|  | ||||
|   // Calculate absolute humidity | ||||
|   const float absolute_humidity = vapor_density(es, hr, temperature_k); | ||||
|  | ||||
|   // Publish absolute humidity | ||||
|   ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity); | ||||
|   this->status_clear_warning(); | ||||
|   this->publish_state(absolute_humidity); | ||||
| } | ||||
|  | ||||
| // Buck equation (https://en.wikipedia.org/wiki/Arden_Buck_equation) | ||||
| // More accurate than Tetens in normal meteorologic conditions | ||||
| float AbsoluteHumidityComponent::es_buck(float temperature_c) { | ||||
|   float a, b, c, d; | ||||
|   if (temperature_c >= 0) { | ||||
|     a = 0.61121; | ||||
|     b = 18.678; | ||||
|     c = 234.5; | ||||
|     d = 257.14; | ||||
|   } else { | ||||
|     a = 0.61115; | ||||
|     b = 18.678; | ||||
|     c = 233.7; | ||||
|     d = 279.82; | ||||
|   } | ||||
|   return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c))); | ||||
| } | ||||
|  | ||||
| // Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation) | ||||
| float AbsoluteHumidityComponent::es_tetens(float temperature_c) { | ||||
|   float a, b; | ||||
|   if (temperature_c >= 0) { | ||||
|     a = 17.27; | ||||
|     b = 237.3; | ||||
|   } else { | ||||
|     a = 21.875; | ||||
|     b = 265.5; | ||||
|   } | ||||
|   return 0.61078 * expf((a * temperature_c) / (temperature_c + b)); | ||||
| } | ||||
|  | ||||
| // Wobus equation | ||||
| // https://wahiduddin.net/calc/density_altitude.htm | ||||
| // https://wahiduddin.net/calc/density_algorithms.htm | ||||
| // Calculate the saturation vapor pressure (kPa) | ||||
| float AbsoluteHumidityComponent::es_wobus(float t) { | ||||
|   // THIS FUNCTION RETURNS THE SATURATION VAPOR PRESSURE ESW (MILLIBARS) | ||||
|   // OVER LIQUID WATER GIVEN THE TEMPERATURE T (CELSIUS). THE POLYNOMIAL | ||||
|   // APPROXIMATION BELOW IS DUE TO HERMAN WOBUS, A MATHEMATICIAN WHO | ||||
|   // WORKED AT THE NAVY WEATHER RESEARCH FACILITY, NORFOLK, VIRGINIA, | ||||
|   // BUT WHO IS NOW RETIRED. THE COEFFICIENTS OF THE POLYNOMIAL WERE | ||||
|   // CHOSEN TO FIT THE VALUES IN TABLE 94 ON PP. 351-353 OF THE SMITH- | ||||
|   // SONIAN METEOROLOGICAL TABLES BY ROLAND LIST (6TH EDITION). THE | ||||
|   // APPROXIMATION IS VALID FOR -50 < T < 100C. | ||||
|   // | ||||
|   //     Baker, Schlatter  17-MAY-1982     Original version. | ||||
|  | ||||
|   const float c0 = +0.99999683e00; | ||||
|   const float c1 = -0.90826951e-02; | ||||
|   const float c2 = +0.78736169e-04; | ||||
|   const float c3 = -0.61117958e-06; | ||||
|   const float c4 = +0.43884187e-08; | ||||
|   const float c5 = -0.29883885e-10; | ||||
|   const float c6 = +0.21874425e-12; | ||||
|   const float c7 = -0.17892321e-14; | ||||
|   const float c8 = +0.11112018e-16; | ||||
|   const float c9 = -0.30994571e-19; | ||||
|   const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9))))))))); | ||||
|   return 0.61078 / pow(p, 8); | ||||
| } | ||||
|  | ||||
| // From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/ | ||||
| // H/T to https://esphome.io/cookbook/bme280_environment.html | ||||
| // H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/ | ||||
| float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) { | ||||
|   // es = saturated vapor pressure (kPa) | ||||
|   // hr = relative humidity [0-1] | ||||
|   // ta = absolute temperature (K) | ||||
|  | ||||
|   const float ea = hr * es * 1000;   // vapor pressure of the air (Pa) | ||||
|   const float mw = 18.01528;         // molar mass of water (g⋅mol⁻¹) | ||||
|   const float r = 8.31446261815324;  // molar gas constant (J⋅K⁻¹) | ||||
|   return (ea * mw) / (r * ta); | ||||
| } | ||||
|  | ||||
| }  // namespace absolute_humidity | ||||
| }  // namespace esphome | ||||
							
								
								
									
										76
									
								
								esphome/components/absolute_humidity/absolute_humidity.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								esphome/components/absolute_humidity/absolute_humidity.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,76 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace absolute_humidity { | ||||
|  | ||||
| /// Enum listing all implemented saturation vapor pressure equations. | ||||
| enum SaturationVaporPressureEquation { | ||||
|   BUCK, | ||||
|   TETENS, | ||||
|   WOBUS, | ||||
| }; | ||||
|  | ||||
| /// This class implements calculation of absolute humidity from temperature and relative humidity. | ||||
| class AbsoluteHumidityComponent : public sensor::Sensor, public Component { | ||||
|  public: | ||||
|   AbsoluteHumidityComponent() = default; | ||||
|  | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; } | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; } | ||||
|   void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void loop() override; | ||||
|  | ||||
|  protected: | ||||
|   void temperature_callback_(float state) { | ||||
|     this->next_update_ = true; | ||||
|     this->temperature_ = state; | ||||
|   } | ||||
|   void humidity_callback_(float state) { | ||||
|     this->next_update_ = true; | ||||
|     this->humidity_ = state; | ||||
|   } | ||||
|  | ||||
|   /** Buck equation for saturation vapor pressure in kPa. | ||||
|    * | ||||
|    * @param temperature_c Air temperature in °C. | ||||
|    */ | ||||
|   static float es_buck(float temperature_c); | ||||
|   /** Tetens equation for saturation vapor pressure in kPa. | ||||
|    * | ||||
|    * @param temperature_c Air temperature in °C. | ||||
|    */ | ||||
|   static float es_tetens(float temperature_c); | ||||
|   /** Wobus equation for saturation vapor pressure in kPa. | ||||
|    * | ||||
|    * @param temperature_c Air temperature in °C. | ||||
|    */ | ||||
|   static float es_wobus(float temperature_c); | ||||
|  | ||||
|   /** Calculate vapor density (absolute humidity) in g/m³. | ||||
|    * | ||||
|    * @param es Saturation vapor pressure in kPa. | ||||
|    * @param hr Relative humidity 0 to 1. | ||||
|    * @param ta Absolute temperature in K. | ||||
|    * @param heater_duration The duration in ms that the heater should turn on for when measuring. | ||||
|    */ | ||||
|   static float vapor_density(float es, float hr, float ta); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|  | ||||
|   bool next_update_{false}; | ||||
|  | ||||
|   float temperature_{NAN}; | ||||
|   float humidity_{NAN}; | ||||
|   SaturationVaporPressureEquation equation_; | ||||
| }; | ||||
|  | ||||
| }  // namespace absolute_humidity | ||||
| }  // namespace esphome | ||||
							
								
								
									
										56
									
								
								esphome/components/absolute_humidity/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/absolute_humidity/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     CONF_EQUATION, | ||||
|     ICON_WATER, | ||||
|     UNIT_GRAMS_PER_CUBIC_METER, | ||||
| ) | ||||
|  | ||||
| absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity") | ||||
| AbsoluteHumidityComponent = absolute_humidity_ns.class_( | ||||
|     "AbsoluteHumidityComponent", sensor.Sensor, cg.Component | ||||
| ) | ||||
|  | ||||
| SaturationVaporPressureEquation = absolute_humidity_ns.enum( | ||||
|     "SaturationVaporPressureEquation" | ||||
| ) | ||||
| EQUATION = { | ||||
|     "BUCK": SaturationVaporPressureEquation.BUCK, | ||||
|     "TETENS": SaturationVaporPressureEquation.TETENS, | ||||
|     "WOBUS": SaturationVaporPressureEquation.WOBUS, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER, | ||||
|         icon=ICON_WATER, | ||||
|         accuracy_decimals=2, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AbsoluteHumidityComponent), | ||||
|             cv.Required(CONF_TEMPERATURE): cv.use_id(sensor.Sensor), | ||||
|             cv.Required(CONF_HUMIDITY): cv.use_id(sensor.Sensor), | ||||
|             cv.Optional(CONF_EQUATION, default="WOBUS"): cv.enum(EQUATION, upper=True), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     temperature_sensor = await cg.get_variable(config[CONF_TEMPERATURE]) | ||||
|     cg.add(var.set_temperature_sensor(temperature_sensor)) | ||||
|  | ||||
|     humidity_sensor = await cg.get_variable(config[CONF_HUMIDITY]) | ||||
|     cg.add(var.set_humidity_sensor(humidity_sensor)) | ||||
|  | ||||
|     cg.add(var.set_equation(config[CONF_EQUATION])) | ||||
| @@ -122,6 +122,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|       // also take into account min_power | ||||
|       auto min_us = this->cycle_time_us * this->min_power / 1000; | ||||
|       this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); | ||||
|  | ||||
|       if (this->method == DIM_METHOD_LEADING_PULSE) { | ||||
|         // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone | ||||
|         // this is for brightness near 99% | ||||
| @@ -202,6 +203,7 @@ void AcDimmer::setup() { | ||||
| #endif | ||||
| } | ||||
| void AcDimmer::write_state(float state) { | ||||
|   state = std::acos(1 - (2 * state)) / 3.14159;  // RMS power compensation | ||||
|   auto new_value = static_cast<uint16_t>(roundf(state * 65535)); | ||||
|   if (new_value != 0 && this->store_.value == 0) | ||||
|     this->store_.init_cycle = this->init_with_half_cycle_; | ||||
|   | ||||
| @@ -1 +1,118 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.const import CONF_INPUT | ||||
|  | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
|  | ||||
| ATTENUATION_MODES = { | ||||
|     "0db": cg.global_ns.ADC_ATTEN_DB_0, | ||||
|     "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, | ||||
|     "6db": cg.global_ns.ADC_ATTEN_DB_6, | ||||
|     "11db": cg.global_ns.ADC_ATTEN_DB_11, | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
|  | ||||
| # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h | ||||
| # pin to adc1 channel mapping | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|     }, | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_adc_pin(value): | ||||
|     if str(value).upper() == "VCC": | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if str(value).upper() == "TEMPERATURE": | ||||
|         return cv.only_on_rp2040("TEMPERATURE") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         variant = get_esp32_variant() | ||||
|         if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: | ||||
|             raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") | ||||
|  | ||||
|         if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: | ||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     if CORE.is_esp8266: | ||||
|         from esphome.components.esp8266.gpio import CONF_ANALOG | ||||
|  | ||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( | ||||
|             value | ||||
|         ) | ||||
|  | ||||
|         if value != 17:  # A0 | ||||
|             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") | ||||
|         return pins.gpio_pin_schema( | ||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||
|         )(value) | ||||
|  | ||||
|     if CORE.is_rp2040: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         if value not in (26, 27, 28, 29): | ||||
|             raise cv.Invalid("RP2040: Only pins 26, 27, 28 and 29 support ADC.") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     raise NotImplementedError | ||||
|   | ||||
| @@ -11,19 +11,38 @@ ADC_MODE(ADC_VCC) | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| #include <hardware/adc.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
| // 13 bits for S3 / 12 bit for all other esp32 variants | ||||
| // create a const to avoid the repated cast to enum | ||||
|  | ||||
| // 13bit for S2, and 12bit for all other esp32 variants | ||||
| #ifdef USE_ESP32 | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit) | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit) | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| extern "C" | ||||
| #endif | ||||
|     void | ||||
|     ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
| #if !defined(USE_ADC_SENSOR_VCC) && !defined(USE_RP2040) | ||||
|   pin_->setup(); | ||||
| #endif | ||||
|  | ||||
| @@ -51,11 +70,17 @@ void ADCSensor::setup() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // adc_gpio_init doesn't exist on ESP32-C3 or ESP32-H2 | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   static bool initialized = false; | ||||
|   if (!initialized) { | ||||
|     adc_init(); | ||||
|     initialized = true; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "ADC '%s' setup finished!", this->get_name().c_str()); | ||||
| } | ||||
|  | ||||
| void ADCSensor::dump_config() { | ||||
| @@ -75,22 +100,29 @@ void ADCSensor::dump_config() { | ||||
|   } else { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_11: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| #endif  // USE_ESP32 | ||||
| #ifdef USE_RP2040 | ||||
|   if (this->is_temperature_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Pin: Temperature"); | ||||
|   } else { | ||||
|     LOG_PIN("  Pin: ", pin_); | ||||
|   } | ||||
| #endif | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| @@ -129,16 +161,16 @@ float ADCSensor::sample() { | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); | ||||
|   raw11 = adc1_get_raw(channel_); | ||||
|   if (raw11 < 4095) { | ||||
|   if (raw11 < ADC_MAX) { | ||||
|     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); | ||||
|     raw6 = adc1_get_raw(channel_); | ||||
|     if (raw6 < 4095) { | ||||
|     if (raw6 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); | ||||
|       raw2 = adc1_get_raw(channel_); | ||||
|       if (raw2 < 4095) { | ||||
|       if (raw2 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); | ||||
|         raw0 = adc1_get_raw(channel_); | ||||
|       } | ||||
| @@ -154,20 +186,43 @@ float ADCSensor::sample() { | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   // Contribution of each value, in range 0-2048 | ||||
|   uint32_t c11 = std::min(raw11, 2048); | ||||
|   uint32_t c6 = 2048 - std::abs(raw6 - 2048); | ||||
|   uint32_t c2 = 2048 - std::abs(raw2 - 2048); | ||||
|   uint32_t c0 = std::min(4095 - raw0, 2048); | ||||
|   // max theoretical csum value is 2048*4 = 8192 | ||||
|   // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) | ||||
|   uint32_t c11 = std::min(raw11, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   // max theoretical csum value is 4096*4 = 16384 | ||||
|   uint32_t csum = c11 + c6 + c2 + c0; | ||||
|  | ||||
|   // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned | ||||
|   // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 | ||||
|   uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
| #endif  // USE_ESP32 | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
| float ADCSensor::sample() { | ||||
|   if (this->is_temperature_) { | ||||
|     adc_set_temp_sensor_enabled(true); | ||||
|     delay(1); | ||||
|     adc_select_input(4); | ||||
|   } else { | ||||
|     uint8_t pin = this->pin_->get_pin(); | ||||
|     adc_gpio_init(pin); | ||||
|     adc_select_input(pin - 26); | ||||
|   } | ||||
|  | ||||
|   int raw = adc_read(); | ||||
|   if (this->is_temperature_) { | ||||
|     adc_set_temp_sensor_enabled(false); | ||||
|   } | ||||
|   if (output_raw_) { | ||||
|     return raw; | ||||
|   } | ||||
|   return raw * 3.3f / 4096.0f; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
| #endif | ||||
|   | ||||
| @@ -38,10 +38,18 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   std::string unique_id() override; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   void set_is_temperature() { is_temperature_ = true; } | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   InternalGPIOPin *pin_; | ||||
|   bool output_raw_{false}; | ||||
|  | ||||
| #ifdef USE_RP2040 | ||||
|   bool is_temperature_{false}; | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
|   adc1_channel_t channel_{}; | ||||
|   | ||||
| @@ -1,124 +1,27 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import sensor, voltage_sampler | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
|     CONF_RAW, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PIN, | ||||
|     CONF_RAW, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import get_esp32_variant | ||||
| from esphome.components.esp32.const import ( | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32H2, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
|  | ||||
| from . import ( | ||||
|     ATTENUATION_MODES, | ||||
|     ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, | ||||
|     validate_adc_pin, | ||||
| ) | ||||
|  | ||||
|  | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
|  | ||||
| ATTENUATION_MODES = { | ||||
|     "0db": cg.global_ns.ADC_ATTEN_DB_0, | ||||
|     "2.5db": cg.global_ns.ADC_ATTEN_DB_2_5, | ||||
|     "6db": cg.global_ns.ADC_ATTEN_DB_6, | ||||
|     "11db": cg.global_ns.ADC_ATTEN_DB_11, | ||||
|     "auto": "auto", | ||||
| } | ||||
|  | ||||
| adc1_channel_t = cg.global_ns.enum("adc1_channel_t") | ||||
|  | ||||
| # From https://github.com/espressif/esp-idf/blob/master/components/driver/include/driver/adc_common.h | ||||
| # pin to adc1 channel mapping | ||||
| ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = { | ||||
|     VARIANT_ESP32: { | ||||
|         36: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         37: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         38: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         39: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         32: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         33: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         34: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         35: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|     }, | ||||
|     VARIANT_ESP32S2: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32S3: { | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         5: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|         6: adc1_channel_t.ADC1_CHANNEL_5, | ||||
|         7: adc1_channel_t.ADC1_CHANNEL_6, | ||||
|         8: adc1_channel_t.ADC1_CHANNEL_7, | ||||
|         9: adc1_channel_t.ADC1_CHANNEL_8, | ||||
|         10: adc1_channel_t.ADC1_CHANNEL_9, | ||||
|     }, | ||||
|     VARIANT_ESP32C3: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
|     VARIANT_ESP32H2: { | ||||
|         0: adc1_channel_t.ADC1_CHANNEL_0, | ||||
|         1: adc1_channel_t.ADC1_CHANNEL_1, | ||||
|         2: adc1_channel_t.ADC1_CHANNEL_2, | ||||
|         3: adc1_channel_t.ADC1_CHANNEL_3, | ||||
|         4: adc1_channel_t.ADC1_CHANNEL_4, | ||||
|     }, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_adc_pin(value): | ||||
|     if str(value).upper() == "VCC": | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         variant = get_esp32_variant() | ||||
|         if variant not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL: | ||||
|             raise cv.Invalid(f"This ESP32 variant ({variant}) is not supported") | ||||
|  | ||||
|         if value not in ESP32_VARIANT_ADC1_PIN_TO_CHANNEL[variant]: | ||||
|             raise cv.Invalid(f"{variant} doesn't support ADC on this pin") | ||||
|         return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|     if CORE.is_esp8266: | ||||
|         from esphome.components.esp8266.gpio import CONF_ANALOG | ||||
|  | ||||
|         value = pins.internal_gpio_pin_number({CONF_ANALOG: True, CONF_INPUT: True})( | ||||
|             value | ||||
|         ) | ||||
|  | ||||
|         if value != 17:  # A0 | ||||
|             raise cv.Invalid("ESP8266: Only pin A0 (GPIO17) supports ADC.") | ||||
|         return pins.gpio_pin_schema( | ||||
|             {CONF_ANALOG: True, CONF_INPUT: True}, internal=True | ||||
|         )(value) | ||||
|  | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def validate_config(config): | ||||
|     if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": | ||||
| @@ -133,6 +36,7 @@ ADCSensor = adc_ns.class_( | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     sensor.sensor_schema( | ||||
|         ADCSensor, | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=2, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
| @@ -140,7 +44,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADCSensor), | ||||
|             cv.Required(CONF_PIN): validate_adc_pin, | ||||
|             cv.Optional(CONF_RAW, default=False): cv.boolean, | ||||
|             cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( | ||||
| @@ -160,6 +63,8 @@ async def to_code(config): | ||||
|  | ||||
|     if config[CONF_PIN] == "VCC": | ||||
|         cg.add_define("USE_ADC_SENSOR_VCC") | ||||
|     elif config[CONF_PIN] == "TEMPERATURE": | ||||
|         cg.add(var.set_is_temperature()) | ||||
|     else: | ||||
|         pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|         cg.add(var.set_pin(pin)) | ||||
|   | ||||
							
								
								
									
										23
									
								
								esphome/components/adc128s102/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/adc128s102/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import spi | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| DEPENDENCIES = ["spi"] | ||||
| MULTI_CONF = True | ||||
| CODEOWNERS = ["@DeerMaximum"] | ||||
|  | ||||
| adc128s102_ns = cg.esphome_ns.namespace("adc128s102") | ||||
| ADC128S102 = adc128s102_ns.class_("ADC128S102", cg.Component, spi.SPIDevice) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ADC128S102), | ||||
|     } | ||||
| ).extend(spi.spi_device_schema(cs_pin_required=True)) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await spi.register_spi_device(var, config) | ||||
							
								
								
									
										35
									
								
								esphome/components/adc128s102/adc128s102.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/adc128s102/adc128s102.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| #include "adc128s102.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc128s102 { | ||||
|  | ||||
| static const char *const TAG = "adc128s102"; | ||||
|  | ||||
| float ADC128S102::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| void ADC128S102::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up adc128s102"); | ||||
|   this->spi_setup(); | ||||
| } | ||||
|  | ||||
| void ADC128S102::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ADC128S102:"); | ||||
|   LOG_PIN("  CS Pin:", this->cs_); | ||||
| } | ||||
|  | ||||
| uint16_t ADC128S102::read_data(uint8_t channel) { | ||||
|   uint8_t control = channel << 3; | ||||
|  | ||||
|   this->enable(); | ||||
|   uint8_t adc_primary_byte = this->transfer_byte(control); | ||||
|   uint8_t adc_secondary_byte = this->transfer_byte(0x00); | ||||
|   this->disable(); | ||||
|  | ||||
|   uint16_t digital_value = adc_primary_byte << 8 | adc_secondary_byte; | ||||
|  | ||||
|   return digital_value; | ||||
| } | ||||
|  | ||||
| }  // namespace adc128s102 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										23
									
								
								esphome/components/adc128s102/adc128s102.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/adc128s102/adc128s102.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/spi/spi.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc128s102 { | ||||
|  | ||||
| class ADC128S102 : public Component, | ||||
|                    public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING, | ||||
|                                          spi::DATA_RATE_10MHZ> { | ||||
|  public: | ||||
|   ADC128S102() = default; | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   uint16_t read_data(uint8_t channel); | ||||
| }; | ||||
|  | ||||
| }  // namespace adc128s102 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										38
									
								
								esphome/components/adc128s102/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								esphome/components/adc128s102/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, voltage_sampler | ||||
| from esphome.const import CONF_ID, CONF_CHANNEL | ||||
|  | ||||
| from .. import adc128s102_ns, ADC128S102 | ||||
|  | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
| DEPENDENCIES = ["adc128s102"] | ||||
|  | ||||
| ADC128S102Sensor = adc128s102_ns.class_( | ||||
|     "ADC128S102Sensor", | ||||
|     sensor.Sensor, | ||||
|     cg.PollingComponent, | ||||
|     voltage_sampler.VoltageSampler, | ||||
| ) | ||||
| CONF_ADC128S102_ID = "adc128s102_id" | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema(ADC128S102Sensor) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(CONF_ADC128S102_ID): cv.use_id(ADC128S102), | ||||
|             cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable( | ||||
|         config[CONF_ID], | ||||
|         config[CONF_CHANNEL], | ||||
|     ) | ||||
|     await cg.register_parented(var, config[CONF_ADC128S102_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await sensor.register_sensor(var, config) | ||||
							
								
								
									
										24
									
								
								esphome/components/adc128s102/sensor/adc128s102_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								esphome/components/adc128s102/sensor/adc128s102_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| #include "adc128s102_sensor.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc128s102 { | ||||
|  | ||||
| static const char *const TAG = "adc128s102.sensor"; | ||||
|  | ||||
| ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {} | ||||
|  | ||||
| float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void ADC128S102Sensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC128S102 Sensor", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: %u", this->channel_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ADC128S102Sensor::sample() { return this->parent_->read_data(this->channel_); } | ||||
| void ADC128S102Sensor::update() { this->publish_state(this->sample()); } | ||||
|  | ||||
| }  // namespace adc128s102 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										29
									
								
								esphome/components/adc128s102/sensor/adc128s102_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/adc128s102/sensor/adc128s102_sensor.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #include "../adc128s102.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc128s102 { | ||||
|  | ||||
| class ADC128S102Sensor : public PollingComponent, | ||||
|                          public Parented<ADC128S102>, | ||||
|                          public sensor::Sensor, | ||||
|                          public voltage_sampler::VoltageSampler { | ||||
|  public: | ||||
|   ADC128S102Sensor(uint8_t channel); | ||||
|  | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   float sample() override; | ||||
|  | ||||
|  protected: | ||||
|   uint8_t channel_; | ||||
| }; | ||||
| }  // namespace adc128s102 | ||||
| }  // namespace esphome | ||||
| @@ -5,6 +5,8 @@ | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "esphome/components/light/addressable_light.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace addressable_light { | ||||
|  | ||||
| @@ -40,6 +42,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom | ||||
|   void setup() override; | ||||
|   void display(); | ||||
|  | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
|  | ||||
|  protected: | ||||
|   int get_width_internal() override; | ||||
|   int get_height_internal() override; | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ade7953 { | ||||
|  | ||||
| @@ -82,7 +84,7 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { | ||||
|     return i2c::ERROR_OK; | ||||
|   } | ||||
|  | ||||
|   InternalGPIOPin *irq_pin_ = nullptr; | ||||
|   InternalGPIOPin *irq_pin_{nullptr}; | ||||
|   bool is_setup_{false}; | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_a_sensor_{nullptr}; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "ads1115"; | ||||
| static const uint8_t ADS1115_REGISTER_CONVERSION = 0x00; | ||||
| static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; | ||||
|  | ||||
| static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111; | ||||
| static const uint8_t ADS1115_DATA_RATE_860_SPS = 0b111;  // 3300_SPS for ADS1015 | ||||
|  | ||||
| void ADS1115Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); | ||||
| @@ -18,6 +18,9 @@ void ADS1115Component::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "Configuring ADS1115..."); | ||||
|  | ||||
|   uint16_t config = 0; | ||||
|   // Clear single-shot bit | ||||
|   //        0b0xxxxxxxxxxxxxxx | ||||
| @@ -77,6 +80,7 @@ void ADS1115Component::dump_config() { | ||||
|     LOG_SENSOR("  ", "Sensor", sensor); | ||||
|     ESP_LOGCONFIG(TAG, "    Multiplexer: %u", sensor->get_multiplexer()); | ||||
|     ESP_LOGCONFIG(TAG, "    Gain: %u", sensor->get_gain()); | ||||
|     ESP_LOGCONFIG(TAG, "    Resolution: %u", sensor->get_resolution()); | ||||
|   } | ||||
| } | ||||
| float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | ||||
| @@ -127,27 +131,45 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | ||||
|     this->status_set_warning(); | ||||
|     return NAN; | ||||
|   } | ||||
|  | ||||
|   if (sensor->get_resolution() == ADS1015_12_BITS) { | ||||
|     bool negative = (raw_conversion >> 15) == 1; | ||||
|  | ||||
|     // shift raw_conversion as it's only 12-bits, left justified | ||||
|     raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS); | ||||
|  | ||||
|     // check if number was negative in order to keep the sign | ||||
|     if (negative) { | ||||
|       // the number was negative | ||||
|       // 1) set the negative bit back | ||||
|       raw_conversion |= 0x8000; | ||||
|       // 2) reset the former (shifted) negative bit | ||||
|       raw_conversion &= 0xF7FF; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   auto signed_conversion = static_cast<int16_t>(raw_conversion); | ||||
|  | ||||
|   float millivolts; | ||||
|   float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f; | ||||
|   switch (sensor->get_gain()) { | ||||
|     case ADS1115_GAIN_6P144: | ||||
|       millivolts = signed_conversion * 0.187500f; | ||||
|       millivolts = (signed_conversion * 6144) / divider; | ||||
|       break; | ||||
|     case ADS1115_GAIN_4P096: | ||||
|       millivolts = signed_conversion * 0.125000f; | ||||
|       millivolts = (signed_conversion * 4096) / divider; | ||||
|       break; | ||||
|     case ADS1115_GAIN_2P048: | ||||
|       millivolts = signed_conversion * 0.062500f; | ||||
|       millivolts = (signed_conversion * 2048) / divider; | ||||
|       break; | ||||
|     case ADS1115_GAIN_1P024: | ||||
|       millivolts = signed_conversion * 0.031250f; | ||||
|       millivolts = (signed_conversion * 1024) / divider; | ||||
|       break; | ||||
|     case ADS1115_GAIN_0P512: | ||||
|       millivolts = signed_conversion * 0.015625f; | ||||
|       millivolts = (signed_conversion * 512) / divider; | ||||
|       break; | ||||
|     case ADS1115_GAIN_0P256: | ||||
|       millivolts = signed_conversion * 0.007813f; | ||||
|       millivolts = (signed_conversion * 256) / divider; | ||||
|       break; | ||||
|     default: | ||||
|       millivolts = NAN; | ||||
|   | ||||
| @@ -5,6 +5,8 @@ | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ads1115 { | ||||
|  | ||||
| @@ -28,6 +30,11 @@ enum ADS1115Gain { | ||||
|   ADS1115_GAIN_0P256 = 0b101, | ||||
| }; | ||||
|  | ||||
| enum ADS1115Resolution { | ||||
|   ADS1115_16_BITS = 16, | ||||
|   ADS1015_12_BITS = 12, | ||||
| }; | ||||
|  | ||||
| class ADS1115Sensor; | ||||
|  | ||||
| class ADS1115Component : public Component, public i2c::I2CDevice { | ||||
| @@ -56,15 +63,17 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol | ||||
|   void update() override; | ||||
|   void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } | ||||
|   void set_gain(ADS1115Gain gain) { gain_ = gain; } | ||||
|  | ||||
|   void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; } | ||||
|   float sample() override; | ||||
|   uint8_t get_multiplexer() const { return multiplexer_; } | ||||
|   uint8_t get_gain() const { return gain_; } | ||||
|   uint8_t get_resolution() const { return resolution_; } | ||||
|  | ||||
|  protected: | ||||
|   ADS1115Component *parent_; | ||||
|   ADS1115Multiplexer multiplexer_; | ||||
|   ADS1115Gain gain_; | ||||
|   ADS1115Resolution resolution_; | ||||
| }; | ||||
|  | ||||
| }  // namespace ads1115 | ||||
|   | ||||
| @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler | ||||
| from esphome.const import ( | ||||
|     CONF_GAIN, | ||||
|     CONF_MULTIPLEXER, | ||||
|     CONF_RESOLUTION, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| @@ -35,6 +36,12 @@ GAIN = { | ||||
|     "0.256": ADS1115Gain.ADS1115_GAIN_0P256, | ||||
| } | ||||
|  | ||||
| ADS1115Resolution = ads1115_ns.enum("ADS1115Resolution") | ||||
| RESOLUTION = { | ||||
|     "16_BITS": ADS1115Resolution.ADS1115_16_BITS, | ||||
|     "12_BITS": ADS1115Resolution.ADS1015_12_BITS, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_gain(value): | ||||
|     if isinstance(value, float): | ||||
| @@ -52,6 +59,7 @@ ADS1115Sensor = ads1115_ns.class_( | ||||
| CONF_ADS1115_ID = "ads1115_id" | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         ADS1115Sensor, | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=3, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
| @@ -59,10 +67,12 @@ CONFIG_SCHEMA = ( | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADS1115Sensor), | ||||
|             cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), | ||||
|             cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), | ||||
|             cv.Required(CONF_GAIN): validate_gain, | ||||
|             cv.Optional(CONF_RESOLUTION, default="16_BITS"): cv.enum( | ||||
|                 RESOLUTION, upper=True, space="_" | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| @@ -77,5 +87,6 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add(var.set_multiplexer(config[CONF_MULTIPLEXER])) | ||||
|     cg.add(var.set_gain(config[CONF_GAIN])) | ||||
|     cg.add(var.set_resolution(config[CONF_RESOLUTION])) | ||||
|  | ||||
|     cg.add(paren.register_sensor(var)) | ||||
|   | ||||
| @@ -122,8 +122,9 @@ void AHT10Component::update() { | ||||
|     this->temperature_sensor_->publish_state(temperature); | ||||
|   } | ||||
|   if (this->humidity_sensor_ != nullptr) { | ||||
|     if (std::isnan(humidity)) | ||||
|     if (std::isnan(humidity)) { | ||||
|       ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); | ||||
|     } | ||||
|     this->humidity_sensor_->publish_state(humidity); | ||||
|   } | ||||
|   this->status_clear_warning(); | ||||
|   | ||||
| @@ -18,8 +18,8 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice { | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_sensor_; | ||||
|   sensor::Sensor *humidity_sensor_; | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace aht10 | ||||
|   | ||||
							
								
								
									
										83
									
								
								esphome/components/airthings_wave_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								esphome/components/airthings_wave_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client | ||||
|  | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_PRESSURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_PERCENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HECTOPASCAL, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TVOC, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||
|     UNIT_PARTS_PER_BILLION, | ||||
|     ICON_RADIATOR, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@ncareau", "@jeromelaban"] | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base") | ||||
| AirthingsWaveBase = airthings_wave_base_ns.class_( | ||||
|     "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode | ||||
| ) | ||||
|  | ||||
|  | ||||
| BASE_SCHEMA = ( | ||||
|     sensor.SENSOR_SCHEMA.extend( | ||||
|         { | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 accuracy_decimals=0, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||
|                 icon=ICON_RADIATOR, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5min")) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def wave_base_to_code(var, config): | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure(sens)) | ||||
|     if CONF_TVOC in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||
|         cg.add(var.set_tvoc(sens)) | ||||
| @@ -0,0 +1,83 @@ | ||||
| #include "airthings_wave_base.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_base { | ||||
|  | ||||
| static const char *const TAG = "airthings_wave_base"; | ||||
|  | ||||
| void AirthingsWaveBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                             esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status == ESP_GATT_OK) { | ||||
|         ESP_LOGI(TAG, "Connected successfully!"); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGW(TAG, "Disconnected!"); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), | ||||
|                  this->sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->handle_ = chr->handle; | ||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||
|  | ||||
|       this->request_read_values_(); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent()->get_conn_id()) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->handle_) { | ||||
|         this->read_sensors(param->read.value, param->read.value_len); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||
|  | ||||
| void AirthingsWaveBase::update() { | ||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||
|     if (!this->parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "Reconnecting to device"); | ||||
|       this->parent()->set_enabled(true); | ||||
|       this->parent()->connect(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Connection in progress"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveBase::request_read_values_() { | ||||
|   auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, | ||||
|                                         ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace airthings_wave_base | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										50
									
								
								esphome/components/airthings_wave_base/airthings_wave_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								esphome/components/airthings_wave_base/airthings_wave_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
| #include <algorithm> | ||||
| #include <iterator> | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_base { | ||||
|  | ||||
| class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode { | ||||
|  public: | ||||
|   AirthingsWaveBase() = default; | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_voc_value_(uint16_t voc); | ||||
|  | ||||
|   virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0; | ||||
|   void request_read_values_(); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|   sensor::Sensor *pressure_sensor_{nullptr}; | ||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||
|  | ||||
|   uint16_t handle_; | ||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||
| }; | ||||
|  | ||||
| }  // namespace airthings_wave_base | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
| @@ -7,105 +7,47 @@ namespace airthings_wave_mini { | ||||
|  | ||||
| static const char *const TAG = "airthings_wave_mini"; | ||||
|  | ||||
| void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                             esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status == ESP_GATT_OK) { | ||||
|         ESP_LOGI(TAG, "Connected successfully!"); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGW(TAG, "Disconnected!"); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->handle_ = chr->handle; | ||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||
|  | ||||
|       request_read_values_(); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent()->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->handle_) { | ||||
|         read_sensors_(param->read.value, param->read.value_len); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
| void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto *value = (WaveMiniReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WaveMiniReadings) <= value_len) { | ||||
|     this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||
|     this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|     this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); | ||||
|     if (is_valid_voc_value_(value->voc)) { | ||||
|     if (this->humidity_sensor_ != nullptr) { | ||||
|       this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||
|     } | ||||
|  | ||||
|     if (this->pressure_sensor_ != nullptr) { | ||||
|       this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|     } | ||||
|  | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); | ||||
|     } | ||||
|  | ||||
|     if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { | ||||
|       this->tvoc_sensor_->publish_state(value->voc); | ||||
|     } | ||||
|  | ||||
|     // This instance must not stay connected | ||||
|     // so other clients can connect to it (e.g. the | ||||
|     // mobile app). | ||||
|     parent()->set_enabled(false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||
|  | ||||
| void AirthingsWaveMini::update() { | ||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||
|     if (!parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "Reconnecting to device"); | ||||
|       parent()->set_enabled(true); | ||||
|       parent()->connect(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Connection in progress"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::request_read_values_() { | ||||
|   auto status = | ||||
|       esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||
|     this->parent()->set_enabled(false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::dump_config() { | ||||
|   // these really don't belong here, but there doesn't seem to be a | ||||
|   // practical way to have the base class use LOG_SENSOR and include | ||||
|   // the TAG from this component | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWaveMini::AirthingsWaveMini() | ||||
|     : PollingComponent(10000), | ||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), | ||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | ||||
| AirthingsWaveMini::AirthingsWaveMini() { | ||||
|   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||
|   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||
| } | ||||
|  | ||||
| }  // namespace airthings_wave_mini | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -2,14 +2,7 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
| #include <algorithm> | ||||
| #include <iterator> | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/components/airthings_wave_base/airthings_wave_base.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_mini { | ||||
| @@ -17,35 +10,14 @@ namespace airthings_wave_mini { | ||||
| static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { | ||||
| class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { | ||||
|  public: | ||||
|   AirthingsWaveMini(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_voc_value_(uint16_t voc); | ||||
|  | ||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); | ||||
|   void request_read_values_(); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|   sensor::Sensor *pressure_sensor_{nullptr}; | ||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||
|  | ||||
|   uint16_t handle_; | ||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||
|   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||
|  | ||||
|   struct WaveMiniReadings { | ||||
|     uint16_t unused01; | ||||
|   | ||||
| @@ -1,82 +1,28 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client | ||||
| from esphome.components import airthings_wave_base | ||||
|  | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_PRESSURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_PERCENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HECTOPASCAL, | ||||
|     CONF_ID, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TVOC, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     UNIT_PARTS_PER_BILLION, | ||||
|     ICON_RADIATOR, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||
|  | ||||
| AUTO_LOAD = ["airthings_wave_base"] | ||||
|  | ||||
| airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") | ||||
| AirthingsWaveMini = airthings_wave_mini_ns.class_( | ||||
|     "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode | ||||
|     "AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AirthingsWaveMini), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 accuracy_decimals=2, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||
|                 icon=ICON_RADIATOR, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5min")) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), | ||||
| CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AirthingsWaveMini), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure(sens)) | ||||
|     if CONF_TVOC in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||
|         cg.add(var.set_tvoc(sens)) | ||||
|     await airthings_wave_base.wave_base_to_code(var, config) | ||||
|   | ||||
| @@ -7,55 +7,7 @@ namespace airthings_wave_plus { | ||||
|  | ||||
| static const char *const TAG = "airthings_wave_plus"; | ||||
|  | ||||
| void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                             esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status == ESP_GATT_OK) { | ||||
|         ESP_LOGI(TAG, "Connected successfully!"); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGW(TAG, "Disconnected!"); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->handle_ = chr->handle; | ||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||
|  | ||||
|       request_read_values_(); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent()->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->handle_) { | ||||
|         read_sensors_(param->read.value, param->read.value_len); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
| void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto *value = (WavePlusReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WavePlusReadings) <= value_len) { | ||||
| @@ -64,26 +16,38 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|     if (value->version == 1) { | ||||
|       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||
|  | ||||
|       this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||
|       if (is_valid_radon_value_(value->radon)) { | ||||
|       if (this->humidity_sensor_ != nullptr) { | ||||
|         this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||
|       } | ||||
|  | ||||
|       if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) { | ||||
|         this->radon_sensor_->publish_state(value->radon); | ||||
|       } | ||||
|       if (is_valid_radon_value_(value->radon_lt)) { | ||||
|  | ||||
|       if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) { | ||||
|         this->radon_long_term_sensor_->publish_state(value->radon_lt); | ||||
|       } | ||||
|       this->temperature_sensor_->publish_state(value->temperature / 100.0f); | ||||
|       this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|       if (is_valid_co2_value_(value->co2)) { | ||||
|  | ||||
|       if (this->temperature_sensor_ != nullptr) { | ||||
|         this->temperature_sensor_->publish_state(value->temperature / 100.0f); | ||||
|       } | ||||
|  | ||||
|       if (this->pressure_sensor_ != nullptr) { | ||||
|         this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|       } | ||||
|  | ||||
|       if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) { | ||||
|         this->co2_sensor_->publish_state(value->co2); | ||||
|       } | ||||
|       if (is_valid_voc_value_(value->voc)) { | ||||
|  | ||||
|       if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { | ||||
|         this->tvoc_sensor_->publish_state(value->voc); | ||||
|       } | ||||
|  | ||||
|       // This instance must not stay connected | ||||
|       // so other clients can connect to it (e.g. the | ||||
|       // mobile app). | ||||
|       parent()->set_enabled(false); | ||||
|       this->parent()->set_enabled(false); | ||||
|     } else { | ||||
|       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); | ||||
|     } | ||||
| @@ -92,44 +56,26 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } | ||||
|  | ||||
| void AirthingsWavePlus::update() { | ||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||
|     if (!parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "Reconnecting to device"); | ||||
|       parent()->set_enabled(true); | ||||
|       parent()->connect(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Connection in progress"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::request_read_values_() { | ||||
|   auto status = | ||||
|       esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::dump_config() { | ||||
|   // these really don't belong here, but there doesn't seem to be a | ||||
|   // practical way to have the base class use LOG_SENSOR and include | ||||
|   // the TAG from this component | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||
|  | ||||
|   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWavePlus::AirthingsWavePlus() | ||||
|     : PollingComponent(10000), | ||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), | ||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | ||||
| AirthingsWavePlus::AirthingsWavePlus() { | ||||
|   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||
|   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||
| } | ||||
|  | ||||
| }  // namespace airthings_wave_plus | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -2,14 +2,7 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
| #include <algorithm> | ||||
| #include <iterator> | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/components/airthings_wave_base/airthings_wave_base.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_plus { | ||||
| @@ -17,43 +10,25 @@ namespace airthings_wave_plus { | ||||
| static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { | ||||
| class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||
|  public: | ||||
|   AirthingsWavePlus(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||
|   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } | ||||
|   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||
|   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_radon_value_(uint16_t radon); | ||||
|   bool is_valid_voc_value_(uint16_t voc); | ||||
|   bool is_valid_co2_value_(uint16_t co2); | ||||
|  | ||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); | ||||
|   void request_read_values_(); | ||||
|   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|   sensor::Sensor *pressure_sensor_{nullptr}; | ||||
|   sensor::Sensor *co2_sensor_{nullptr}; | ||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||
|  | ||||
|   uint16_t handle_; | ||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||
|  | ||||
|   struct WavePlusReadings { | ||||
|     uint8_t version; | ||||
|   | ||||
| @@ -1,116 +1,64 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client | ||||
| from esphome.components import sensor, airthings_wave_base | ||||
|  | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_PRESSURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_PERCENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HECTOPASCAL, | ||||
|     ICON_RADIOACTIVE, | ||||
|     CONF_ID, | ||||
|     CONF_RADON, | ||||
|     CONF_RADON_LONG_TERM, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TVOC, | ||||
|     CONF_CO2, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|     UNIT_PARTS_PER_MILLION, | ||||
|     UNIT_PARTS_PER_BILLION, | ||||
|     ICON_RADIATOR, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||
|  | ||||
| AUTO_LOAD = ["airthings_wave_base"] | ||||
|  | ||||
| airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | ||||
| AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||
|     "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode | ||||
|     "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 accuracy_decimals=0, | ||||
|             ), | ||||
|             cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||
|                 icon=ICON_RADIATOR, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5min")) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), | ||||
| CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||
|         cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|             icon=ICON_RADIOACTIVE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|             icon=ICON_RADIOACTIVE, | ||||
|             accuracy_decimals=0, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|         cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||
|             unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||
|             accuracy_decimals=0, | ||||
|             device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|             state_class=STATE_CLASS_MEASUREMENT, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await airthings_wave_base.wave_base_to_code(var, config) | ||||
|  | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if CONF_RADON in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_RADON]) | ||||
|         cg.add(var.set_radon(sens)) | ||||
|     if CONF_RADON_LONG_TERM in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) | ||||
|         cg.add(var.set_radon_long_term(sens)) | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure(sens)) | ||||
|     if CONF_CO2 in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_CO2]) | ||||
|         cg.add(var.set_co2(sens)) | ||||
|     if CONF_TVOC in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||
|         cg.add(var.set_tvoc(sens)) | ||||
|   | ||||
							
								
								
									
										165
									
								
								esphome/components/alarm_control_panel/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								esphome/components/alarm_control_panel/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| from esphome.core import CORE, coroutine_with_priority | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_CODE, | ||||
| ) | ||||
| from esphome.cpp_helpers import setup_entity | ||||
|  | ||||
| CODEOWNERS = ["@grahambrown11"] | ||||
| IS_PLATFORM_COMPONENT = True | ||||
|  | ||||
| CONF_ON_TRIGGERED = "on_triggered" | ||||
| CONF_ON_CLEARED = "on_cleared" | ||||
|  | ||||
| alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") | ||||
| AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) | ||||
|  | ||||
| StateTrigger = alarm_control_panel_ns.class_( | ||||
|     "StateTrigger", automation.Trigger.template() | ||||
| ) | ||||
| TriggeredTrigger = alarm_control_panel_ns.class_( | ||||
|     "TriggeredTrigger", automation.Trigger.template() | ||||
| ) | ||||
| ClearedTrigger = alarm_control_panel_ns.class_( | ||||
|     "ClearedTrigger", automation.Trigger.template() | ||||
| ) | ||||
| ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) | ||||
| ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action) | ||||
| DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action) | ||||
| PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action) | ||||
| TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action) | ||||
| AlarmControlPanelCondition = alarm_control_panel_ns.class_( | ||||
|     "AlarmControlPanelCondition", automation.Condition | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||
|         cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||
|         cv.Optional(CONF_CODE): cv.templatable(cv.string), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def setup_alarm_control_panel_core_(var, config): | ||||
|     await setup_entity(var, config) | ||||
|     for conf in config.get(CONF_ON_STATE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     for conf in config.get(CONF_ON_TRIGGERED, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     for conf in config.get(CONF_ON_CLEARED, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|  | ||||
| async def register_alarm_control_panel(var, config): | ||||
|     if not CORE.has_id(config[CONF_ID]): | ||||
|         var = cg.Pvariable(config[CONF_ID], var) | ||||
|     cg.add(cg.App.register_alarm_control_panel(var)) | ||||
|     await setup_alarm_control_panel_core_(var, config) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||
| ) | ||||
| async def alarm_action_arm_away_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     if CONF_CODE in config: | ||||
|         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||
|         cg.add(var.set_code(templatable_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||
| ) | ||||
| async def alarm_action_arm_home_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     if CONF_CODE in config: | ||||
|         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||
|         cg.add(var.set_code(templatable_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||
| ) | ||||
| async def alarm_action_disarm_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     if CONF_CODE in config: | ||||
|         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||
|         cg.add(var.set_code(templatable_)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||
| ) | ||||
| async def alarm_action_pending_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||
| ) | ||||
| async def alarm_action_trigger_to_code(config, action_id, template_arg, args): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| @automation.register_condition( | ||||
|     "alarm_control_panel.is_armed", | ||||
|     AlarmControlPanelCondition, | ||||
|     ALARM_CONTROL_PANEL_CONDITION_SCHEMA, | ||||
| ) | ||||
| async def alarm_control_panel_is_armed_to_code( | ||||
|     config, condition_id, template_arg, args | ||||
| ): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     return cg.new_Pvariable(condition_id, template_arg, paren) | ||||
|  | ||||
|  | ||||
| @coroutine_with_priority(100.0) | ||||
| async def to_code(config): | ||||
|     cg.add_global(alarm_control_panel_ns.using) | ||||
|     cg.add_define("USE_ALARM_CONTROL_PANEL") | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user