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", |   "name": "ESPHome Dev", | ||||||
|   "image": "esphome/esphome-lint:dev", |   "image": "ghcr.io/esphome/esphome-lint:dev", | ||||||
|   "postCreateCommand": [ |   "postCreateCommand": [ | ||||||
|     "script/devcontainer-post-create" |     "script/devcontainer-post-create" | ||||||
|   ], |   ], | ||||||
|  |   "containerEnv": { | ||||||
|  |     "DEVCONTAINER": "1" | ||||||
|  |   }, | ||||||
|   "runArgs": [ |   "runArgs": [ | ||||||
|     "--privileged", |     "--privileged", | ||||||
|     "-e", |     "-e", | ||||||
|     "ESPHOME_DASHBOARD_USE_PING=1" |     "ESPHOME_DASHBOARD_USE_PING=1" | ||||||
|   ], |   ], | ||||||
|   "appPort": 6052, |   "appPort": 6052, | ||||||
|   "extensions": [ |   "customizations": { | ||||||
|     // python |     "vscode": { | ||||||
|     "ms-python.python", |       "extensions": [ | ||||||
|     "visualstudioexptteam.vscodeintellicode", |         // python | ||||||
|     // yaml |         "ms-python.python", | ||||||
|     "redhat.vscode-yaml", |         "visualstudioexptteam.vscodeintellicode", | ||||||
|     // cpp |         // yaml | ||||||
|     "ms-vscode.cpptools", |         "redhat.vscode-yaml", | ||||||
|     // editorconfig |         // cpp | ||||||
|     "editorconfig.editorconfig", |         "ms-vscode.cpptools", | ||||||
|   ], |         // editorconfig | ||||||
|   "settings": { |         "editorconfig.editorconfig", | ||||||
|     "python.languageServer": "Pylance", |       ], | ||||||
|     "python.pythonPath": "/usr/bin/python3", |       "settings": { | ||||||
|     "python.linting.pylintEnabled": true, |         "python.languageServer": "Pylance", | ||||||
|     "python.linting.enabled": true, |         "python.pythonPath": "/usr/bin/python3", | ||||||
|     "python.formatting.provider": "black", |         "python.linting.pylintEnabled": true, | ||||||
|     "editor.formatOnPaste": false, |         "python.linting.enabled": true, | ||||||
|     "editor.formatOnSave": true, |         "python.formatting.provider": "black", | ||||||
|     "editor.formatOnType": true, |         "editor.formatOnPaste": false, | ||||||
|     "files.trimTrailingWhitespace": true, |         "editor.formatOnSave": true, | ||||||
|     "terminal.integrated.defaultProfile.linux": "bash", |         "editor.formatOnType": true, | ||||||
|     "yaml.customTags": [ |         "files.trimTrailingWhitespace": true, | ||||||
|       "!secret scalar", |         "terminal.integrated.defaultProfile.linux": "bash", | ||||||
|       "!lambda scalar", |         "yaml.customTags": [ | ||||||
|       "!include_dir_named scalar", |           "!secret scalar", | ||||||
|       "!include_dir_list scalar", |           "!lambda scalar", | ||||||
|       "!include_dir_merge_list scalar", |           "!extend scalar", | ||||||
|       "!include_dir_merge_named scalar" |           "!include_dir_named scalar", | ||||||
|     ], |           "!include_dir_list scalar", | ||||||
|     "files.exclude": { |           "!include_dir_merge_list scalar", | ||||||
|       "**/.git": true, |           "!include_dir_merge_named scalar" | ||||||
|       "**/.DS_Store": true, |         ], | ||||||
|       "**/*.pyc": { |         "files.exclude": { | ||||||
|         "when": "$(basename).py" |           "**/.git": true, | ||||||
|       }, |           "**/.DS_Store": true, | ||||||
|       "**/__pycache__": true |           "**/*.pyc": { | ||||||
|     }, |             "when": "$(basename).py" | ||||||
|     "files.associations": { |           }, | ||||||
|       "**/.vscode/*.json": "jsonc" |           "**/__pycache__": true | ||||||
|     }, |         }, | ||||||
|     "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", |         "files.associations": { | ||||||
|  |           "**/.vscode/*.json": "jsonc" | ||||||
|  |         }, | ||||||
|  |         "C_Cpp.clang_format_path": "/usr/bin/clang-format-13" | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -25,10 +25,9 @@ indent_size = 2 | |||||||
| [*.{yaml,yml}] | [*.{yaml,yml}] | ||||||
| indent_style = space | indent_style = space | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
| quote_type = single | quote_type = double | ||||||
|  |  | ||||||
| # JSON | # JSON | ||||||
| [*.json] | [*.json] | ||||||
| indent_style = space | indent_style = space | ||||||
| indent_size = 2 | indent_size = 2 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| # Normalize line endings to LF in the repository | # Normalize line endings to LF in the repository | ||||||
| * text eol=lf | * 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 | # These are supported funding model platforms | ||||||
|  |  | ||||||
| custom: https://www.nabucasa.com | 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 | blank_issues_enabled: false | ||||||
| contact_links: | contact_links: | ||||||
|   - name: Issue Tracker |   - name: Issue Tracker | ||||||
| @@ -5,8 +6,10 @@ contact_links: | |||||||
|     about: Please create bug reports in the dedicated issue tracker. |     about: Please create bug reports in the dedicated issue tracker. | ||||||
|   - name: Feature Request Tracker |   - name: Feature Request Tracker | ||||||
|     url: https://github.com/esphome/feature-requests |     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 |   - name: Frequently Asked Question | ||||||
|     url: https://esphome.io/guides/faq.html |     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 | ## Types of changes | ||||||
|  |  | ||||||
| @@ -18,6 +18,7 @@ Quick description and explanation of changes | |||||||
| - [ ] ESP32 | - [ ] ESP32 | ||||||
| - [ ] ESP32 IDF | - [ ] ESP32 IDF | ||||||
| - [ ] ESP8266 | - [ ] ESP8266 | ||||||
|  | - [ ] RP2040 | ||||||
|  |  | ||||||
| ## Example entry for `config.yaml`: | ## Example entry for `config.yaml`: | ||||||
| <!-- | <!-- | ||||||
| @@ -35,6 +36,6 @@ Quick description and explanation of changes | |||||||
| ## Checklist: | ## Checklist: | ||||||
|   - [ ] The code change is tested and works locally. |   - [ ] The code change is tested and works locally. | ||||||
|   - [ ] Tests have been added to verify that the new code works (under `tests/` folder). |   - [ ] Tests have been added to verify that the new code works (under `tests/` folder). | ||||||
|    |  | ||||||
| If user exposed functionality or configuration variables are added/changed: | If user exposed functionality or configuration variables are added/changed: | ||||||
|   - [ ] Documentation added/updated in [esphome-docs](https://github.com/esphome/esphome-docs). |   - [ ] 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 | version: 2 | ||||||
| updates: | updates: | ||||||
|   - package-ecosystem: "pip" |   - package-ecosystem: pip | ||||||
|     directory: "/" |     directory: "/" | ||||||
|     schedule: |     schedule: | ||||||
|       interval: "daily" |       interval: daily | ||||||
|     ignore: |     ignore: | ||||||
|       # Hypotehsis is only used for testing and is updated quite often |       # Hypotehsis is only used for testing and is updated quite often | ||||||
|       - dependency-name: hypothesis |       - 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 | name: CI for docker images | ||||||
|  |  | ||||||
| # Only run when docker paths change | # Only run when docker paths change | ||||||
|  | # yamllint disable-line rule:truthy | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [dev, beta, release] |     branches: [dev, beta, release] | ||||||
|     paths: |     paths: | ||||||
|       - 'docker/**' |       - "docker/**" | ||||||
|       - '.github/workflows/**' |       - ".github/workflows/**" | ||||||
|       - 'requirements*.txt' |       - "requirements*.txt" | ||||||
|       - 'platformio.ini' |       - "platformio.ini" | ||||||
|  |       - "script/platformio_install_deps.py" | ||||||
|  |  | ||||||
|   pull_request: |   pull_request: | ||||||
|     paths: |     paths: | ||||||
|       - 'docker/**' |       - "docker/**" | ||||||
|       - '.github/workflows/**' |       - ".github/workflows/**" | ||||||
|       - 'requirements*.txt' |       - "requirements*.txt" | ||||||
|       - 'platformio.ini' |       - "platformio.ini" | ||||||
|  |       - "script/platformio_install_deps.py" | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
|   contents: read |   contents: read | ||||||
|   packages: 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: | jobs: | ||||||
|   check-docker: |   check-docker: | ||||||
|     name: Build docker containers |     name: Build docker containers | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     strategy: |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         arch: [amd64, armv7, aarch64] |         arch: [amd64, armv7, aarch64] | ||||||
|         build_type: ["ha-addon", "docker", "lint"] |         build_type: ["ha-addon", "docker", "lint"] | ||||||
|     steps: |     steps: | ||||||
|     - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|     - name: Set up Python |       - name: Set up Python | ||||||
|       uses: actions/setup-python@v2 |         uses: actions/setup-python@v4 | ||||||
|       with: |         with: | ||||||
|         python-version: '3.9' |           python-version: "3.9" | ||||||
|     - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|       uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v2 | ||||||
|     - name: Set up QEMU |       - name: Set up QEMU | ||||||
|       uses: docker/setup-qemu-action@v1 |         uses: docker/setup-qemu-action@v2 | ||||||
|  |  | ||||||
|     - name: Set TAG |       - name: Set TAG | ||||||
|       run: | |         run: | | ||||||
|         echo "TAG=check" >> $GITHUB_ENV |           echo "TAG=check" >> $GITHUB_ENV | ||||||
|  |  | ||||||
|     - name: Run build |       - name: Run build | ||||||
|       run: | |         run: | | ||||||
|         docker/build.py \ |           docker/build.py \ | ||||||
|           --tag "${TAG}" \ |             --tag "${TAG}" \ | ||||||
|           --arch "${{ matrix.arch }}" \ |             --arch "${{ matrix.arch }}" \ | ||||||
|           --build-type "${{ matrix.build_type }}" \ |             --build-type "${{ matrix.build_type }}" \ | ||||||
|           build |             build | ||||||
|   | |||||||
							
								
								
									
										386
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										386
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,54 +1,275 @@ | |||||||
|  | --- | ||||||
| name: CI | name: CI | ||||||
|  |  | ||||||
|  | # yamllint disable-line rule:truthy | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [dev, beta, release] |     branches: [dev, beta, release] | ||||||
|  |  | ||||||
|   pull_request: |   pull_request: | ||||||
|  |   merge_group: | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
|   contents: read |   contents: read | ||||||
|  |  | ||||||
|  | env: | ||||||
|  |   DEFAULT_PYTHON: "3.9" | ||||||
|  |   PYUPGRADE_TARGET: "--py39-plus" | ||||||
|  |   CLANG_FORMAT_VERSION: "13.0.1" | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|  |   # yamllint disable-line rule:line-length | ||||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} |   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   ci: |   common: | ||||||
|     name: ${{ matrix.name }} |     name: Create common environment | ||||||
|     runs-on: ubuntu-latest |     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: |     strategy: | ||||||
|       fail-fast: false |       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: |       matrix: | ||||||
|         include: |         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 |           - id: clang-tidy | ||||||
|             name: Run script/clang-tidy for ESP8266 |             name: Run script/clang-tidy for ESP8266 | ||||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 |             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||||
| @@ -75,92 +296,61 @@ jobs: | |||||||
|             pio_cache_key: tidyesp32-idf |             pio_cache_key: tidyesp32-idf | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - name: Check out code from GitHub | ||||||
|       - name: Set up Python |         uses: actions/checkout@v3.5.2 | ||||||
|         uses: actions/setup-python@v2 |       - name: Restore Python | ||||||
|         id: python |         uses: ./.github/actions/restore-python | ||||||
|         with: |         with: | ||||||
|           python-version: '3.7' |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|  |           cache-key: ${{ needs.common.outputs.cache-key }} | ||||||
|       - 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 |  | ||||||
|       - name: Cache platformio |       - name: Cache platformio | ||||||
|         uses: actions/cache@v2 |         uses: actions/cache@v3.3.1 | ||||||
|         with: |         with: | ||||||
|           path: ~/.platformio |           path: ~/.platformio | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} |           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||||
|         if: matrix.id == 'test' || matrix.id == 'clang-tidy' |  | ||||||
|  |  | ||||||
|       - name: Install clang tools |       - name: Install clang-tidy | ||||||
|         run: | |         run: sudo apt-get install clang-tidy-11 | ||||||
|           sudo apt-get install \ |  | ||||||
|               clang-format-11 \ |  | ||||||
|               clang-tidy-11 |  | ||||||
|         if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' |  | ||||||
|  |  | ||||||
|       - name: Register problem matchers |       - name: Register problem matchers | ||||||
|         run: | |         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/gcc.json" | ||||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.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 |       - name: Run clang-tidy | ||||||
|         run: | |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|           script/clang-tidy --all-headers --fix ${{ matrix.options }} |           script/clang-tidy --all-headers --fix ${{ matrix.options }} | ||||||
|         if: matrix.id == 'clang-tidy' |  | ||||||
|         env: |         env: | ||||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder |           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps |           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||||
|  |  | ||||||
|       - name: Suggested changes |       - name: Suggested changes | ||||||
|         run: script/ci-suggest-changes |         run: script/ci-suggest-changes | ||||||
|         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 | name: Lock | ||||||
|  |  | ||||||
|  | # yamllint disable-line rule:truthy | ||||||
| on: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: '30 0 * * *' |     - cron: "30 0 * * *" | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -16,7 +18,7 @@ jobs: | |||||||
|   lock: |   lock: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: dessant/lock-threads@v3 |       - uses: dessant/lock-threads@v4 | ||||||
|         with: |         with: | ||||||
|           pr-inactive-days: "1" |           pr-inactive-days: "1" | ||||||
|           pr-lock-reason: "" |           pr-lock-reason: "" | ||||||
|   | |||||||
							
								
								
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										171
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,7 @@ | |||||||
|  | --- | ||||||
| name: Publish Release | name: Publish Release | ||||||
|  |  | ||||||
|  | # yamllint disable-line rule:truthy | ||||||
| on: | on: | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|   release: |   release: | ||||||
| @@ -17,9 +19,10 @@ jobs: | |||||||
|     outputs: |     outputs: | ||||||
|       tag: ${{ steps.tag.outputs.tag }} |       tag: ${{ steps.tag.outputs.tag }} | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|       - name: Get tag |       - name: Get tag | ||||||
|         id: tag |         id: tag | ||||||
|  |         # yamllint disable rule:line-length | ||||||
|         run: | |         run: | | ||||||
|           if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then |           if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then | ||||||
|             TAG="${GITHUB_REF#refs/tags/}" |             TAG="${GITHUB_REF#refs/tags/}" | ||||||
| @@ -27,23 +30,30 @@ jobs: | |||||||
|             TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") |             TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") | ||||||
|             today="$(date --utc '+%Y%m%d')" |             today="$(date --utc '+%Y%m%d')" | ||||||
|             TAG="${TAG}${today}" |             TAG="${TAG}${today}" | ||||||
|  |             BRANCH=${GITHUB_REF#refs/heads/} | ||||||
|  |             if [[ "$BRANCH" != "dev" ]]; then | ||||||
|  |               TAG="${TAG}-${BRANCH}" | ||||||
|  |             fi | ||||||
|           fi |           fi | ||||||
|           echo "::set-output name=tag::${TAG}" |           echo "tag=${TAG}" >> $GITHUB_OUTPUT | ||||||
|  |         # yamllint enable rule:line-length | ||||||
|  |  | ||||||
|   deploy-pypi: |   deploy-pypi: | ||||||
|     name: Build and publish to PyPi |     name: Build and publish to PyPi | ||||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' |     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|       - name: Set up Python |       - name: Set up Python | ||||||
|         uses: actions/setup-python@v1 |         uses: actions/setup-python@v4 | ||||||
|         with: |         with: | ||||||
|           python-version: '3.x' |           python-version: "3.x" | ||||||
|       - name: Set up python environment |       - name: Set up python environment | ||||||
|  |         env: | ||||||
|  |           ESPHOME_NO_VENV: 1 | ||||||
|         run: | |         run: | | ||||||
|           script/setup |           script/setup | ||||||
|           pip install setuptools wheel twine |           pip install twine | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: python setup.py sdist bdist_wheel |         run: python setup.py sdist bdist_wheel | ||||||
|       - name: Upload |       - name: Upload | ||||||
| @@ -53,102 +63,95 @@ jobs: | |||||||
|         run: twine upload dist/* |         run: twine upload dist/* | ||||||
|  |  | ||||||
|   deploy-docker: |   deploy-docker: | ||||||
|     name: Build and publish docker containers |     name: Build and publish ESPHome ${{ matrix.image.title}} | ||||||
|     if: github.repository == 'esphome/esphome' |     if: github.repository == 'esphome/esphome' | ||||||
|     permissions: |     permissions: | ||||||
|       contents: read |       contents: read | ||||||
|       packages: write |       packages: write | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     continue-on-error: ${{ matrix.image.title == 'lint' }} | ||||||
|     needs: [init] |     needs: [init] | ||||||
|     strategy: |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|       matrix: |       matrix: | ||||||
|         arch: [amd64, armv7, aarch64] |         image: | ||||||
|         build_type: ["ha-addon", "docker", "lint"] |           - 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: |     steps: | ||||||
|     - uses: actions/checkout@v2 |       - uses: actions/checkout@v3 | ||||||
|     - name: Set up Python |       - name: Set up Python | ||||||
|       uses: actions/setup-python@v2 |         uses: actions/setup-python@v4 | ||||||
|       with: |         with: | ||||||
|         python-version: '3.9' |           python-version: "3.9" | ||||||
|  |  | ||||||
|     - name: Set up Docker Buildx |       - name: Set up Docker Buildx | ||||||
|       uses: docker/setup-buildx-action@v1 |         uses: docker/setup-buildx-action@v2 | ||||||
|     - name: Set up QEMU |       - name: Set up QEMU | ||||||
|       uses: docker/setup-qemu-action@v1 |         uses: docker/setup-qemu-action@v2 | ||||||
|  |  | ||||||
|     - name: Log in to docker hub |       - name: Log in to docker hub | ||||||
|       uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|       with: |         with: | ||||||
|         username: ${{ secrets.DOCKER_USER }} |           username: ${{ secrets.DOCKER_USER }} | ||||||
|         password: ${{ secrets.DOCKER_PASSWORD }} |           password: ${{ secrets.DOCKER_PASSWORD }} | ||||||
|     - name: Log in to the GitHub container registry |       - name: Log in to the GitHub container registry | ||||||
|       uses: docker/login-action@v1 |         uses: docker/login-action@v2 | ||||||
|       with: |         with: | ||||||
|           registry: ghcr.io |           registry: ghcr.io | ||||||
|           username: ${{ github.actor }} |           username: ${{ github.actor }} | ||||||
|           password: ${{ secrets.GITHUB_TOKEN }} |           password: ${{ secrets.GITHUB_TOKEN }} | ||||||
|  |  | ||||||
|     - name: Build and push |       - name: Generate short tags | ||||||
|       run: | |         id: tags | ||||||
|         docker/build.py \ |         run: | | ||||||
|           --tag "${{ needs.init.outputs.tag }}" \ |           docker/generate_tags.py \ | ||||||
|           --arch "${{ matrix.arch }}" \ |             --tag "${{ needs.init.outputs.tag }}" \ | ||||||
|           --build-type "${{ matrix.build_type }}" \ |             --suffix "${{ matrix.image.suffix }}" | ||||||
|           build \ |  | ||||||
|           --push |  | ||||||
|  |  | ||||||
|   deploy-docker-manifest: |       - name: Build and push | ||||||
|     if: github.repository == 'esphome/esphome' |         uses: docker/build-push-action@v4 | ||||||
|     permissions: |         with: | ||||||
|       contents: read |           context: . | ||||||
|       packages: write |           file: ./docker/Dockerfile | ||||||
|     runs-on: ubuntu-latest |           platforms: linux/amd64,linux/arm/v7,linux/arm64 | ||||||
|     needs: [init, deploy-docker] |           target: ${{ matrix.image.target }} | ||||||
|     strategy: |           push: true | ||||||
|       matrix: |           # yamllint disable rule:line-length | ||||||
|         build_type: ["ha-addon", "docker", "lint"] |           cache-from: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }} | ||||||
|     steps: |           cache-to: type=registry,ref=ghcr.io/${{ steps.tags.outputs.image }}:cache-${{ steps.tags.outputs.channel }},mode=max | ||||||
|     - uses: actions/checkout@v2 |           # yamllint enable rule:line-length | ||||||
|     - name: Set up Python |           tags: ${{ steps.tags.outputs.tags }} | ||||||
|       uses: actions/setup-python@v2 |           build-args: | | ||||||
|       with: |             BASEIMGTYPE=${{ matrix.image.baseimg }} | ||||||
|         python-version: '3.9' |             BUILD_VERSION=${{ needs.init.outputs.tag }} | ||||||
|     - name: Enable experimental manifest support |  | ||||||
|       run: | |  | ||||||
|         mkdir -p ~/.docker |  | ||||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json |  | ||||||
|  |  | ||||||
|     - name: Log in to docker hub |   deploy-ha-addon-repo: | ||||||
|       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: |  | ||||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' |     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     needs: [deploy-docker] |     needs: [deploy-docker] | ||||||
|     steps: |     steps: | ||||||
|       - env: |       - name: Trigger Workflow | ||||||
|           TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} |         uses: actions/github-script@v6 | ||||||
|         run: | |         with: | ||||||
|           TAG="${GITHUB_REF#refs/tags/}" |           github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||||
|           curl \ |           script: | | ||||||
|             -u ":$TOKEN" \ |             github.rest.actions.createWorkflowDispatch({ | ||||||
|             -X POST \ |               owner: "esphome", | ||||||
|             -H "Accept: application/vnd.github.v3+json" \ |               repo: "home-assistant-addon", | ||||||
|             https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ |               workflow_id: "bump-version.yml", | ||||||
|             -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" |               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 | name: Stale | ||||||
|  |  | ||||||
|  | # yamllint disable-line rule:truthy | ||||||
| on: | on: | ||||||
|   schedule: |   schedule: | ||||||
|     - cron: '30 0 * * *' |     - cron: "30 0 * * *" | ||||||
|   workflow_dispatch: |   workflow_dispatch: | ||||||
|  |  | ||||||
| permissions: | permissions: | ||||||
| @@ -16,7 +18,7 @@ jobs: | |||||||
|   stale: |   stale: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v4 |       - uses: actions/stale@v8 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: 90 |           days-before-pr-stale: 90 | ||||||
|           days-before-pr-close: 7 |           days-before-pr-close: 7 | ||||||
| @@ -24,18 +26,19 @@ jobs: | |||||||
|           days-before-issue-close: -1 |           days-before-issue-close: -1 | ||||||
|           remove-stale-when-updated: true |           remove-stale-when-updated: true | ||||||
|           stale-pr-label: "stale" |           stale-pr-label: "stale" | ||||||
|           exempt-pr-labels: "no-stale" |           exempt-pr-labels: "not-stale" | ||||||
|           stale-pr-message: > |           stale-pr-message: > | ||||||
|             There hasn't been any activity on this pull request recently. This |             There hasn't been any activity on this pull request recently. This | ||||||
|             pull request has been automatically marked as stale because of that |             pull request has been automatically marked as stale because of that | ||||||
|             and will be closed if no further activity occurs within 7 days. |             and will be closed if no further activity occurs within 7 days. | ||||||
|             Thank you for your contributions. |             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: |   close-issues: | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/stale@v4 |       - uses: actions/stale@v8 | ||||||
|         with: |         with: | ||||||
|           days-before-pr-stale: -1 |           days-before-pr-stale: -1 | ||||||
|           days-before-pr-close: -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/ | ||||||
| env.bak/ | env.bak/ | ||||||
| venv.bak/ | venv.bak/ | ||||||
|  | venv-*/ | ||||||
|  |  | ||||||
| # mypy | # mypy | ||||||
| .mypy_cache/ | .mypy_cache/ | ||||||
| @@ -127,3 +128,5 @@ tests/.esphome/ | |||||||
|  |  | ||||||
| sdkconfig.* | sdkconfig.* | ||||||
| !sdkconfig.defaults | !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 for more information | ||||||
| # See https://pre-commit.com/hooks.html for more hooks | # See https://pre-commit.com/hooks.html for more hooks | ||||||
| repos: | repos: | ||||||
|   - repo: https://github.com/ambv/black |   - repo: https://github.com/psf/black | ||||||
|     rev: 22.1.0 |     rev: 23.3.0 | ||||||
|     hooks: |     hooks: | ||||||
|     - id: black |       - id: black | ||||||
|       args: |         args: | ||||||
|         - --safe |           - --safe | ||||||
|         - --quiet |           - --quiet | ||||||
|       files: ^((esphome|script|tests)/.+)?[^/]+\.py$ |         files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||||
|   - repo: https://gitlab.com/pycqa/flake8 |   - repo: https://github.com/PyCQA/flake8 | ||||||
|     rev: 4.0.1 |     rev: 6.0.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: flake8 |       - id: flake8 | ||||||
|         additional_dependencies: |         additional_dependencies: | ||||||
| @@ -25,3 +26,8 @@ repos: | |||||||
|           - --branch=dev |           - --branch=dev | ||||||
|           - --branch=release |           - --branch=release | ||||||
|           - --branch=beta |           - --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", |   "version": "2.0.0", | ||||||
|   "tasks": [ |   "tasks": [ | ||||||
|     { |     { | ||||||
|       "label": "run", |       "label": "Run Dashboard", | ||||||
|       "type": "shell", |       "type": "shell", | ||||||
|       "command": "python3 -m esphome dashboard config/", |       "command": "${command:python.interpreterPath}", | ||||||
|  |       "args": [ | ||||||
|  |         "-m", | ||||||
|  |         "esphome", | ||||||
|  |         "dashboard", | ||||||
|  |         "config/" | ||||||
|  |       ], | ||||||
|       "problemMatcher": [] |       "problemMatcher": [] | ||||||
|     }, |     }, | ||||||
|     { |     { | ||||||
|       "label": "clang-tidy", |       "label": "clang-tidy", | ||||||
|       "type": "shell", |       "type": "shell", | ||||||
|       "command": "./script/clang-tidy", |       "command": "${command:python.interpreterPath}", | ||||||
|  |       "args": [ | ||||||
|  |         "./script/clang-tidy" | ||||||
|  |       ], | ||||||
|       "problemMatcher": [ |       "problemMatcher": [ | ||||||
|         { |         { | ||||||
|           "owner": "clang-tidy", |           "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 | esphome/core/* @esphome/core | ||||||
|  |  | ||||||
| # Integrations | # Integrations | ||||||
|  | esphome/components/absolute_humidity/* @DAVe3283 | ||||||
| esphome/components/ac_dimmer/* @glmnet | esphome/components/ac_dimmer/* @glmnet | ||||||
| esphome/components/adc/* @esphome/core | esphome/components/adc/* @esphome/core | ||||||
|  | esphome/components/adc128s102/* @DeerMaximum | ||||||
| esphome/components/addressable_light/* @justfalter | esphome/components/addressable_light/* @justfalter | ||||||
| esphome/components/airthings_ble/* @jeromelaban | esphome/components/airthings_ble/* @jeromelaban | ||||||
|  | esphome/components/airthings_wave_base/* @jeromelaban @ncareau | ||||||
| esphome/components/airthings_wave_mini/* @ncareau | esphome/components/airthings_wave_mini/* @ncareau | ||||||
| esphome/components/airthings_wave_plus/* @jeromelaban | esphome/components/airthings_wave_plus/* @jeromelaban | ||||||
|  | esphome/components/alarm_control_panel/* @grahambrown11 | ||||||
| esphome/components/am43/* @buxtronix | esphome/components/am43/* @buxtronix | ||||||
| esphome/components/am43/cover/* @buxtronix | esphome/components/am43/cover/* @buxtronix | ||||||
|  | esphome/components/am43/sensor/* @buxtronix | ||||||
|  | esphome/components/analog_threshold/* @ianchi | ||||||
| esphome/components/animation/* @syndlex | esphome/components/animation/* @syndlex | ||||||
| esphome/components/anova/* @buxtronix | esphome/components/anova/* @buxtronix | ||||||
| esphome/components/api/* @OttoWinter | esphome/components/api/* @OttoWinter | ||||||
|  | esphome/components/as7341/* @mrgnr | ||||||
| esphome/components/async_tcp/* @OttoWinter | esphome/components/async_tcp/* @OttoWinter | ||||||
| esphome/components/atc_mithermometer/* @ahpohl | esphome/components/atc_mithermometer/* @ahpohl | ||||||
| esphome/components/b_parasite/* @rbaron | esphome/components/b_parasite/* @rbaron | ||||||
| esphome/components/ballu/* @bazuchan | esphome/components/ballu/* @bazuchan | ||||||
| esphome/components/bang_bang/* @OttoWinter | 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/binary_sensor/* @esphome/core | ||||||
|  | esphome/components/bl0939/* @ziceva | ||||||
| esphome/components/bl0940/* @tobias- | esphome/components/bl0940/* @tobias- | ||||||
|  | esphome/components/bl0942/* @dbuezas | ||||||
| esphome/components/ble_client/* @buxtronix | esphome/components/ble_client/* @buxtronix | ||||||
|  | esphome/components/bluetooth_proxy/* @jesserockz | ||||||
| esphome/components/bme680_bsec/* @trvrnrth | esphome/components/bme680_bsec/* @trvrnrth | ||||||
| esphome/components/bmp3xx/* @martgras | esphome/components/bmp3xx/* @martgras | ||||||
|  | esphome/components/bp1658cj/* @Cossid | ||||||
|  | esphome/components/bp5758d/* @Cossid | ||||||
| esphome/components/button/* @esphome/core | esphome/components/button/* @esphome/core | ||||||
| esphome/components/canbus/* @danielschramm @mvturnho | esphome/components/canbus/* @danielschramm @mvturnho | ||||||
| esphome/components/cap1188/* @MrEditor97 | esphome/components/cap1188/* @MrEditor97 | ||||||
| @@ -42,56 +58,98 @@ esphome/components/climate/* @esphome/core | |||||||
| esphome/components/climate_ir/* @glmnet | esphome/components/climate_ir/* @glmnet | ||||||
| esphome/components/color_temperature/* @jesserockz | esphome/components/color_temperature/* @jesserockz | ||||||
| esphome/components/coolix/* @glmnet | esphome/components/coolix/* @glmnet | ||||||
|  | esphome/components/copy/* @OttoWinter | ||||||
| esphome/components/cover/* @esphome/core | esphome/components/cover/* @esphome/core | ||||||
| esphome/components/cs5460a/* @balrog-kun | esphome/components/cs5460a/* @balrog-kun | ||||||
| esphome/components/cse7761/* @berfenger | esphome/components/cse7761/* @berfenger | ||||||
| esphome/components/ct_clamp/* @jesserockz | esphome/components/ct_clamp/* @jesserockz | ||||||
| esphome/components/current_based/* @djwmarcx | esphome/components/current_based/* @djwmarcx | ||||||
|  | esphome/components/dac7678/* @NickB1 | ||||||
|  | esphome/components/daikin_brc/* @hagak | ||||||
| esphome/components/daly_bms/* @s1lvi0 | esphome/components/daly_bms/* @s1lvi0 | ||||||
| esphome/components/dashboard_import/* @esphome/core | esphome/components/dashboard_import/* @esphome/core | ||||||
| esphome/components/debug/* @OttoWinter | esphome/components/debug/* @OttoWinter | ||||||
|  | esphome/components/delonghi/* @grob6000 | ||||||
| esphome/components/dfplayer/* @glmnet | esphome/components/dfplayer/* @glmnet | ||||||
| esphome/components/dht/* @OttoWinter | esphome/components/dht/* @OttoWinter | ||||||
|  | esphome/components/display_menu_base/* @numo68 | ||||||
|  | esphome/components/dps310/* @kbx81 | ||||||
| esphome/components/ds1307/* @badbadc0ffee | esphome/components/ds1307/* @badbadc0ffee | ||||||
| esphome/components/dsmr/* @glmnet @zuidwijk | esphome/components/dsmr/* @glmnet @zuidwijk | ||||||
|  | esphome/components/ee895/* @Stock-M | ||||||
| esphome/components/ektf2232/* @jesserockz | esphome/components/ektf2232/* @jesserockz | ||||||
|  | esphome/components/ens210/* @itn3rd77 | ||||||
| esphome/components/esp32/* @esphome/core | esphome/components/esp32/* @esphome/core | ||||||
| esphome/components/esp32_ble/* @jesserockz | esphome/components/esp32_ble/* @jesserockz | ||||||
|  | esphome/components/esp32_ble_client/* @jesserockz | ||||||
| esphome/components/esp32_ble_server/* @jesserockz | esphome/components/esp32_ble_server/* @jesserockz | ||||||
| esphome/components/esp32_camera_web_server/* @ayufan | esphome/components/esp32_camera_web_server/* @ayufan | ||||||
| esphome/components/esp32_can/* @Sympatron | esphome/components/esp32_can/* @Sympatron | ||||||
| esphome/components/esp32_improv/* @jesserockz | esphome/components/esp32_improv/* @jesserockz | ||||||
|  | esphome/components/esp32_rmt_led_strip/* @jesserockz | ||||||
| esphome/components/esp8266/* @esphome/core | esphome/components/esp8266/* @esphome/core | ||||||
|  | esphome/components/ethernet_info/* @gtjadsonsantos | ||||||
| esphome/components/exposure_notifications/* @OttoWinter | esphome/components/exposure_notifications/* @OttoWinter | ||||||
| esphome/components/ezo/* @ssieb | esphome/components/ezo/* @ssieb | ||||||
|  | esphome/components/ezo_pmp/* @carlos-sarmiento | ||||||
|  | esphome/components/factory_reset/* @anatoly-savchenkov | ||||||
| esphome/components/fastled_base/* @OttoWinter | esphome/components/fastled_base/* @OttoWinter | ||||||
|  | esphome/components/feedback/* @ianchi | ||||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||||
|  | esphome/components/fs3000/* @kahrendt | ||||||
| esphome/components/globals/* @esphome/core | esphome/components/globals/* @esphome/core | ||||||
|  | esphome/components/gp8403/* @jesserockz | ||||||
| esphome/components/gpio/* @esphome/core | esphome/components/gpio/* @esphome/core | ||||||
| esphome/components/gps/* @coogle | esphome/components/gps/* @coogle | ||||||
| esphome/components/graph/* @synco | esphome/components/graph/* @synco | ||||||
| esphome/components/growatt_solar/* @leeuwte | esphome/components/growatt_solar/* @leeuwte | ||||||
|  | esphome/components/haier/* @paveldn | ||||||
| esphome/components/havells_solar/* @sourabhjaiswal | esphome/components/havells_solar/* @sourabhjaiswal | ||||||
| esphome/components/hbridge/fan/* @WeekendWarrior | esphome/components/hbridge/fan/* @WeekendWarrior | ||||||
| esphome/components/hbridge/light/* @DotNetDann | esphome/components/hbridge/light/* @DotNetDann | ||||||
| esphome/components/heatpumpir/* @rob-deutsch | esphome/components/heatpumpir/* @rob-deutsch | ||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
|  | esphome/components/hm3301/* @freekode | ||||||
| esphome/components/homeassistant/* @OttoWinter | esphome/components/homeassistant/* @OttoWinter | ||||||
|  | esphome/components/honeywellabp/* @RubyBailey | ||||||
|  | esphome/components/host/* @esphome/core | ||||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | 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/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/improv_serial/* @esphome/core | ||||||
| esphome/components/ina260/* @MrEditor97 | esphome/components/ina260/* @MrEditor97 | ||||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||||
| esphome/components/inkplate6/* @jesserockz | esphome/components/inkplate6/* @jesserockz | ||||||
| esphome/components/integration/* @OttoWinter | esphome/components/integration/* @OttoWinter | ||||||
|  | esphome/components/internal_temperature/* @Mat931 | ||||||
| esphome/components/interval/* @esphome/core | esphome/components/interval/* @esphome/core | ||||||
| esphome/components/json/* @OttoWinter | esphome/components/json/* @OttoWinter | ||||||
| esphome/components/kalman_combinator/* @Cat-Ion | 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/ledc/* @OttoWinter | ||||||
| esphome/components/light/* @esphome/core | esphome/components/light/* @esphome/core | ||||||
|  | esphome/components/lilygo_t5_47/touchscreen/* @jesserockz | ||||||
|  | esphome/components/lock/* @esphome/core | ||||||
| esphome/components/logger/* @esphome/core | esphome/components/logger/* @esphome/core | ||||||
| esphome/components/ltr390/* @sjtrny | 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/max7219digit/* @rspaargaren | ||||||
|  | esphome/components/max9611/* @mckaymatthew | ||||||
| esphome/components/mcp23008/* @jesserockz | esphome/components/mcp23008/* @jesserockz | ||||||
| esphome/components/mcp23017/* @jesserockz | esphome/components/mcp23017/* @jesserockz | ||||||
| esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | esphome/components/mcp23s08/* @SenexCrenshaw @jesserockz | ||||||
| @@ -101,20 +159,34 @@ esphome/components/mcp23x17_base/* @jesserockz | |||||||
| esphome/components/mcp23xxx_base/* @jesserockz | esphome/components/mcp23xxx_base/* @jesserockz | ||||||
| esphome/components/mcp2515/* @danielschramm @mvturnho | esphome/components/mcp2515/* @danielschramm @mvturnho | ||||||
| esphome/components/mcp3204/* @rsumner | esphome/components/mcp3204/* @rsumner | ||||||
|  | esphome/components/mcp4728/* @berfenger | ||||||
| esphome/components/mcp47a1/* @jesserockz | esphome/components/mcp47a1/* @jesserockz | ||||||
|  | esphome/components/mcp9600/* @MrEditor97 | ||||||
| esphome/components/mcp9808/* @k7hpn | esphome/components/mcp9808/* @k7hpn | ||||||
| esphome/components/md5/* @esphome/core | esphome/components/md5/* @esphome/core | ||||||
| esphome/components/mdns/* @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/* @dudanov | ||||||
| esphome/components/midea_ir/* @dudanov | esphome/components/midea_ir/* @dudanov | ||||||
| esphome/components/mitsubishi/* @RubyBailey | 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/* @martgras | ||||||
| esphome/components/modbus_controller/binary_sensor/* @martgras | esphome/components/modbus_controller/binary_sensor/* @martgras | ||||||
| esphome/components/modbus_controller/number/* @martgras | esphome/components/modbus_controller/number/* @martgras | ||||||
| esphome/components/modbus_controller/output/* @martgras | esphome/components/modbus_controller/output/* @martgras | ||||||
|  | esphome/components/modbus_controller/select/* @martgras @stegm | ||||||
| esphome/components/modbus_controller/sensor/* @martgras | esphome/components/modbus_controller/sensor/* @martgras | ||||||
| esphome/components/modbus_controller/switch/* @martgras | esphome/components/modbus_controller/switch/* @martgras | ||||||
| esphome/components/modbus_controller/text_sensor/* @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/network/* @esphome/core | ||||||
| esphome/components/nextion/* @senexcrenshaw | esphome/components/nextion/* @senexcrenshaw | ||||||
| esphome/components/nextion/binary_sensor/* @senexcrenshaw | esphome/components/nextion/binary_sensor/* @senexcrenshaw | ||||||
| @@ -125,6 +197,9 @@ esphome/components/nfc/* @jesserockz | |||||||
| esphome/components/number/* @esphome/core | esphome/components/number/* @esphome/core | ||||||
| esphome/components/ota/* @esphome/core | esphome/components/ota/* @esphome/core | ||||||
| esphome/components/output/* @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/pid/* @OttoWinter | ||||||
| esphome/components/pipsolar/* @andreashergert1984 | esphome/components/pipsolar/* @andreashergert1984 | ||||||
| esphome/components/pm1006/* @habbie | esphome/components/pm1006/* @habbie | ||||||
| @@ -135,31 +210,53 @@ esphome/components/pn532_spi/* @OttoWinter @jesserockz | |||||||
| esphome/components/power_supply/* @esphome/core | esphome/components/power_supply/* @esphome/core | ||||||
| esphome/components/preferences/* @esphome/core | esphome/components/preferences/* @esphome/core | ||||||
| esphome/components/psram/* @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/pvvx_mithermometer/* @pasiz | ||||||
|  | esphome/components/qmp6988/* @andrewpc | ||||||
| esphome/components/qr_code/* @wjtje | esphome/components/qr_code/* @wjtje | ||||||
|  | esphome/components/radon_eye_ble/* @jeffeb3 | ||||||
|  | esphome/components/radon_eye_rd200/* @jeffeb3 | ||||||
| esphome/components/rc522/* @glmnet | esphome/components/rc522/* @glmnet | ||||||
| esphome/components/rc522_i2c/* @glmnet | esphome/components/rc522_i2c/* @glmnet | ||||||
| esphome/components/rc522_spi/* @glmnet | esphome/components/rc522_spi/* @glmnet | ||||||
| esphome/components/restart/* @esphome/core | esphome/components/restart/* @esphome/core | ||||||
| esphome/components/rf_bridge/* @jesserockz | esphome/components/rf_bridge/* @jesserockz | ||||||
| esphome/components/rgbct/* @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/rtttl/* @glmnet | ||||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | esphome/components/safe_mode/* @jsuanet @paulmonigatti | ||||||
| esphome/components/scd4x/* @sjtrny | esphome/components/scd4x/* @martgras @sjtrny | ||||||
| esphome/components/script/* @esphome/core | esphome/components/script/* @esphome/core | ||||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||||
| esphome/components/sdp3x/* @Azimath | esphome/components/sdp3x/* @Azimath | ||||||
| esphome/components/selec_meter/* @sourabhjaiswal | esphome/components/selec_meter/* @sourabhjaiswal | ||||||
| esphome/components/select/* @esphome/core | 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/sensor/* @esphome/core | ||||||
| esphome/components/sgp40/* @SenexCrenshaw | esphome/components/sgp40/* @SenexCrenshaw | ||||||
|  | esphome/components/sgp4x/* @SenexCrenshaw @martgras | ||||||
|  | esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||||
| esphome/components/sht4x/* @sjtrny | esphome/components/sht4x/* @sjtrny | ||||||
| esphome/components/shutdown/* @esphome/core @jsuanet | esphome/components/shutdown/* @esphome/core @jsuanet | ||||||
|  | esphome/components/sigma_delta_output/* @Cat-Ion | ||||||
| esphome/components/sim800l/* @glmnet | 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/socket/* @esphome/core | ||||||
|  | esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||||
|  | esphome/components/speaker/* @jesserockz | ||||||
| esphome/components/spi/* @esphome/core | esphome/components/spi/* @esphome/core | ||||||
|  | esphome/components/sprinkler/* @kbx81 | ||||||
|  | esphome/components/sps30/* @martgras | ||||||
| esphome/components/ssd1322_base/* @kbx81 | esphome/components/ssd1322_base/* @kbx81 | ||||||
| esphome/components/ssd1322_spi/* @kbx81 | esphome/components/ssd1322_spi/* @kbx81 | ||||||
| esphome/components/ssd1325_base/* @kbx81 | esphome/components/ssd1325_base/* @kbx81 | ||||||
| @@ -180,12 +277,18 @@ esphome/components/switch/* @esphome/core | |||||||
| esphome/components/t6615/* @tylermenezes | esphome/components/t6615/* @tylermenezes | ||||||
| esphome/components/tca9548a/* @andreashergert1984 | esphome/components/tca9548a/* @andreashergert1984 | ||||||
| esphome/components/tcl112/* @glmnet | esphome/components/tcl112/* @glmnet | ||||||
|  | esphome/components/tee501/* @Stock-M | ||||||
| esphome/components/teleinfo/* @0hax | esphome/components/teleinfo/* @0hax | ||||||
|  | esphome/components/template/alarm_control_panel/* @grahambrown11 | ||||||
| esphome/components/thermostat/* @kbx81 | esphome/components/thermostat/* @kbx81 | ||||||
| esphome/components/time/* @OttoWinter | esphome/components/time/* @OttoWinter | ||||||
| esphome/components/tlc5947/* @rnauber | esphome/components/tlc5947/* @rnauber | ||||||
|  | esphome/components/tm1621/* @Philippe12 | ||||||
| esphome/components/tm1637/* @glmnet | esphome/components/tm1637/* @glmnet | ||||||
|  | esphome/components/tm1638/* @skykingjwc | ||||||
|  | esphome/components/tm1651/* @freekode | ||||||
| esphome/components/tmp102/* @timsavage | esphome/components/tmp102/* @timsavage | ||||||
|  | esphome/components/tmp1075/* @sybrenstuvel | ||||||
| esphome/components/tmp117/* @Azimath | esphome/components/tmp117/* @Azimath | ||||||
| esphome/components/tof10120/* @wstrzalka | esphome/components/tof10120/* @wstrzalka | ||||||
| esphome/components/toshiba/* @kbx81 | esphome/components/toshiba/* @kbx81 | ||||||
| @@ -194,16 +297,27 @@ esphome/components/tsl2591/* @wjcarpenter | |||||||
| esphome/components/tuya/binary_sensor/* @jesserockz | esphome/components/tuya/binary_sensor/* @jesserockz | ||||||
| esphome/components/tuya/climate/* @jesserockz | esphome/components/tuya/climate/* @jesserockz | ||||||
| esphome/components/tuya/number/* @frankiboy1 | esphome/components/tuya/number/* @frankiboy1 | ||||||
|  | esphome/components/tuya/select/* @bearpawmaxim | ||||||
| esphome/components/tuya/sensor/* @jesserockz | esphome/components/tuya/sensor/* @jesserockz | ||||||
| esphome/components/tuya/switch/* @jesserockz | esphome/components/tuya/switch/* @jesserockz | ||||||
| esphome/components/tuya/text_sensor/* @dentra | esphome/components/tuya/text_sensor/* @dentra | ||||||
| esphome/components/uart/* @esphome/core | esphome/components/uart/* @esphome/core | ||||||
|  | esphome/components/ufire_ec/* @pvizeli | ||||||
|  | esphome/components/ufire_ise/* @pvizeli | ||||||
| esphome/components/ultrasonic/* @OttoWinter | esphome/components/ultrasonic/* @OttoWinter | ||||||
|  | esphome/components/vbus/* @ssieb | ||||||
| esphome/components/version/* @esphome/core | esphome/components/version/* @esphome/core | ||||||
|  | esphome/components/voice_assistant/* @jesserockz | ||||||
| esphome/components/wake_on_lan/* @willwill2will54 | esphome/components/wake_on_lan/* @willwill2will54 | ||||||
| esphome/components/web_server_base/* @OttoWinter | esphome/components/web_server_base/* @OttoWinter | ||||||
| esphome/components/whirlpool/* @glmnet | 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_lywsd03mmc/* @ahpohl | ||||||
| esphome/components/xiaomi_mhoc303/* @drug123 | esphome/components/xiaomi_mhoc303/* @drug123 | ||||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | 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: | Things to note when contributing: | ||||||
|  |  | ||||||
|  - Please test your changes :) |  - 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) |    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. |    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 |  - 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 LICENSE | ||||||
| include README.md | include README.md | ||||||
| include requirements.txt | include requirements.txt | ||||||
| include esphome/dashboard/templates/*.html | recursive-include esphome *.cpp *.h *.tcc *.c | ||||||
| recursive-include esphome/dashboard/static *.ico *.js *.css *.woff* LICENSE |  | ||||||
| recursive-include esphome *.cpp *.h *.tcc |  | ||||||
| recursive-include esphome *.py.script | recursive-include esphome *.py.script | ||||||
| recursive-include esphome LICENSE.txt | recursive-include esphome LICENSE.txt | ||||||
|   | |||||||
| @@ -5,29 +5,32 @@ | |||||||
| # One of "docker", "hassio" | # One of "docker", "hassio" | ||||||
| ARG BASEIMGTYPE=docker | ARG BASEIMGTYPE=docker | ||||||
|  |  | ||||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.2.3 AS base-hassio-amd64 | # https://github.com/hassio-addons/addon-debian-base/releases | ||||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.2.3 AS base-hassio-arm64 | FROM ghcr.io/hassio-addons/debian-base:6.2.3 AS base-hassio | ||||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.2.3 AS base-hassio-armv7 | # https://hub.docker.com/_/debian?tab=tags&page=1&name=bullseye | ||||||
| FROM debian:bullseye-20220125-slim AS base-docker-amd64 | FROM debian:bullseye-20230208-slim AS base-docker | ||||||
| FROM debian:bullseye-20220125-slim AS base-docker-arm64 |  | ||||||
| FROM debian:bullseye-20220125-slim AS base-docker-armv7 |  | ||||||
|  |  | ||||||
| # Use TARGETARCH/TARGETVARIANT defined by docker | FROM base-${BASEIMGTYPE} AS base | ||||||
| # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope |  | ||||||
| FROM base-${BASEIMGTYPE}-${TARGETARCH}${TARGETVARIANT} AS base | ARG TARGETARCH | ||||||
|  | ARG TARGETVARIANT | ||||||
|  |  | ||||||
| RUN \ | RUN \ | ||||||
|     apt-get update \ |     apt-get update \ | ||||||
|     # Use pinned versions so that we get updates with build caching |     # Use pinned versions so that we get updates with build caching | ||||||
|     && apt-get install -y --no-install-recommends \ |     && apt-get install -y --no-install-recommends \ | ||||||
|         python3=3.9.2-3 \ |         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-setuptools=52.0.0-4 \ | ||||||
|         python3-pil=8.1.2+dfsg-0.3+deb11u1 \ |         python3-pil=8.1.2+dfsg-0.3+deb11u1 \ | ||||||
|         python3-cryptography=3.3.2-1 \ |         python3-cryptography=3.3.2-1 \ | ||||||
|  |         python3-venv=3.9.2-3 \ | ||||||
|         iputils-ping=3:20210202-1 \ |         iputils-ping=3:20210202-1 \ | ||||||
|         git=1:2.30.2-1 \ |         git=1:2.30.2-1+deb11u2 \ | ||||||
|         curl=7.74.0-1.3+deb11u1 \ |         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 \ |     && rm -rf \ | ||||||
|         /tmp/* \ |         /tmp/* \ | ||||||
|         /var/{cache,log}/* \ |         /var/{cache,log}/* \ | ||||||
| @@ -39,29 +42,35 @@ ENV \ | |||||||
|   # Store globally installed pio libs in /piolibs |   # Store globally installed pio libs in /piolibs | ||||||
|   PLATFORMIO_GLOBALLIB_DIR=/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 \ | RUN \ | ||||||
|     # Ubuntu python3-pip is missing wheel |     # Ubuntu python3-pip is missing wheel | ||||||
|     pip3 install --no-cache-dir \ |     pip3 install --no-cache-dir \ | ||||||
|         wheel==0.37.1 \ |         wheel==0.37.1 \ | ||||||
|         platformio==5.2.4 \ |         platformio==6.1.7 \ | ||||||
|     # Change some platformio settings |     # Change some platformio settings | ||||||
|     && platformio settings set enable_telemetry No \ |     && platformio settings set enable_telemetry No \ | ||||||
|     && platformio settings set check_libraries_interval 1000000 \ |  | ||||||
|     && platformio settings set check_platformio_interval 1000000 \ |     && platformio settings set check_platformio_interval 1000000 \ | ||||||
|     && platformio settings set check_platforms_interval 1000000 \ |  | ||||||
|     && mkdir -p /piolibs |     && 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 ======================= | # ======================= docker-type image ======================= | ||||||
| FROM base AS docker | 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 and install | ||||||
| COPY . /esphome | COPY . /esphome | ||||||
| RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome | RUN pip3 install --no-cache-dir --no-use-pep517 -e /esphome | ||||||
| @@ -93,7 +102,7 @@ RUN \ | |||||||
|     apt-get update \ |     apt-get update \ | ||||||
|     # Use pinned versions so that we get updates with build caching |     # Use pinned versions so that we get updates with build caching | ||||||
|     && apt-get install -y --no-install-recommends \ |     && apt-get install -y --no-install-recommends \ | ||||||
|         nginx=1.18.0-6.1 \ |         nginx-light=1.18.0-6.1+deb11u3 \ | ||||||
|     && rm -rf \ |     && rm -rf \ | ||||||
|         /tmp/* \ |         /tmp/* \ | ||||||
|         /var/{cache,log}/* \ |         /var/{cache,log}/* \ | ||||||
| @@ -102,13 +111,7 @@ RUN \ | |||||||
| ARG BUILD_VERSION=dev | ARG BUILD_VERSION=dev | ||||||
|  |  | ||||||
| # Copy root filesystem | # Copy root filesystem | ||||||
| COPY docker/hassio-rootfs/ / | COPY docker/ha-addon-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 esphome and install | # Copy esphome and install | ||||||
| COPY . /esphome | COPY . /esphome | ||||||
| @@ -135,11 +138,11 @@ RUN \ | |||||||
|     apt-get update \ |     apt-get update \ | ||||||
|     # Use pinned versions so that we get updates with build caching |     # Use pinned versions so that we get updates with build caching | ||||||
|     && apt-get install -y --no-install-recommends \ |     && 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 \ |         clang-tidy-11=1:11.0.1-2 \ | ||||||
|         patch=2.7.6-7 \ |         patch=2.7.6-7 \ | ||||||
|         software-properties-common=0.96.20.2-2.1 \ |         software-properties-common=0.96.20.2-2.1 \ | ||||||
|         nano=5.4-2 \ |         nano=5.4-2+deb11u2 \ | ||||||
|         build-essential=12.9 \ |         build-essential=12.9 \ | ||||||
|         python3-dev=3.9.2-3 \ |         python3-dev=3.9.2-3 \ | ||||||
|     && rm -rf \ |     && rm -rf \ | ||||||
| @@ -147,10 +150,8 @@ RUN \ | |||||||
|         /var/{cache,log}/* \ |         /var/{cache,log}/* \ | ||||||
|         /var/lib/apt/lists/* |         /var/lib/apt/lists/* | ||||||
|  |  | ||||||
| COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py platformio.ini / | COPY requirements_test.txt / | ||||||
| RUN \ | RUN pip3 install --no-cache-dir -r /requirements_test.txt | ||||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ |  | ||||||
|     && /platformio_install_deps.py /platformio.ini |  | ||||||
|  |  | ||||||
| VOLUME ["/esphome"] | VOLUME ["/esphome"] | ||||||
| WORKDIR /esphome | WORKDIR /esphome | ||||||
|   | |||||||
| @@ -8,32 +8,49 @@ import re | |||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
| CHANNEL_DEV = 'dev' | CHANNEL_DEV = "dev" | ||||||
| CHANNEL_BETA = 'beta' | CHANNEL_BETA = "beta" | ||||||
| CHANNEL_RELEASE = 'release' | CHANNEL_RELEASE = "release" | ||||||
| CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] | CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] | ||||||
|  |  | ||||||
| ARCH_AMD64 = 'amd64' | ARCH_AMD64 = "amd64" | ||||||
| ARCH_ARMV7 = 'armv7' | ARCH_ARMV7 = "armv7" | ||||||
| ARCH_AARCH64 = 'aarch64' | ARCH_AARCH64 = "aarch64" | ||||||
| ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] | ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] | ||||||
|  |  | ||||||
| TYPE_DOCKER = 'docker' | TYPE_DOCKER = "docker" | ||||||
| TYPE_HA_ADDON = 'ha-addon' | TYPE_HA_ADDON = "ha-addon" | ||||||
| TYPE_LINT = 'lint' | TYPE_LINT = "lint" | ||||||
| TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | ||||||
|  |  | ||||||
|  |  | ||||||
| parser = argparse.ArgumentParser() | 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( | ||||||
| parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") |     "--tag", | ||||||
| parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") |     type=str, | ||||||
| parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") |     required=True, | ||||||
| subparsers = parser.add_subparsers(help="Action to perform", dest="command", 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 = 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("--push", help="Also push the images", action="store_true") | ||||||
| build_parser.add_argument("--load", help="Load the docker image locally", action="store_true") | build_parser.add_argument( | ||||||
| manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") |     "--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) | @dataclass(frozen=True) | ||||||
| @@ -49,7 +66,7 @@ class DockerParams: | |||||||
|         prefix = { |         prefix = { | ||||||
|             TYPE_DOCKER: "esphome/esphome", |             TYPE_DOCKER: "esphome/esphome", | ||||||
|             TYPE_HA_ADDON: "esphome/esphome-hassio", |             TYPE_HA_ADDON: "esphome/esphome-hassio", | ||||||
|             TYPE_LINT: "esphome/esphome-lint" |             TYPE_LINT: "esphome/esphome-lint", | ||||||
|         }[build_type] |         }[build_type] | ||||||
|         build_to = f"{prefix}-{arch}" |         build_to = f"{prefix}-{arch}" | ||||||
|         baseimgtype = { |         baseimgtype = { | ||||||
| @@ -88,10 +105,12 @@ def main(): | |||||||
|                 sys.exit(1) |                 sys.exit(1) | ||||||
|  |  | ||||||
|     # detect channel from tag |     # 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: |     if match is None: | ||||||
|         channel = CHANNEL_DEV |         channel = CHANNEL_DEV | ||||||
|     elif match.group(1) is None: |     elif match.group(2) is None: | ||||||
|  |         major_minor_version = match.group(1) | ||||||
|         channel = CHANNEL_RELEASE |         channel = CHANNEL_RELEASE | ||||||
|     else: |     else: | ||||||
|         channel = CHANNEL_BETA |         channel = CHANNEL_BETA | ||||||
| @@ -106,6 +125,11 @@ def main(): | |||||||
|         tags_to_push.append("beta") |         tags_to_push.append("beta") | ||||||
|         tags_to_push.append("latest") |         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": |     if args.command == "build": | ||||||
|         # 1. pull cache image |         # 1. pull cache image | ||||||
|         params = DockerParams.for_type_arch(args.build_type, args.arch) |         params = DockerParams.for_type_arch(args.build_type, args.arch) | ||||||
| @@ -121,13 +145,21 @@ def main(): | |||||||
|  |  | ||||||
|         # 3. build |         # 3. build | ||||||
|         cmd = [ |         cmd = [ | ||||||
|             "docker", "buildx", "build", |             "docker", | ||||||
|             "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", |             "buildx", | ||||||
|             "--build-arg", f"BUILD_VERSION={args.tag}", |             "build", | ||||||
|             "--cache-from", f"type=registry,ref={cache_img}", |             "--build-arg", | ||||||
|             "--file", "docker/Dockerfile", |             f"BASEIMGTYPE={params.baseimgtype}", | ||||||
|             "--platform", params.platform, |             "--build-arg", | ||||||
|             "--target", params.target, |             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: |         for img in imgs: | ||||||
|             cmd += ["--tag", img] |             cmd += ["--tag", img] | ||||||
| @@ -153,9 +185,7 @@ def main(): | |||||||
|             run_command(*cmd) |             run_command(*cmd) | ||||||
|         # 2. Push manifests |         # 2. Push manifests | ||||||
|         for target in targets: |         for target in targets: | ||||||
|             run_command( |             run_command("docker", "manifest", "push", target) | ||||||
|                 "docker", "manifest", "push", target |  | ||||||
|             ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| if __name__ == "__main__": | 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_http_version          1.1; | ||||||
| proxy_ignore_client_abort off; | proxy_ignore_client_abort   off; | ||||||
| proxy_read_timeout 86400s; | proxy_read_timeout          86400s; | ||||||
| proxy_redirect off; | proxy_redirect              off; | ||||||
| proxy_send_timeout 86400s; | proxy_send_timeout          86400s; | ||||||
| proxy_max_temp_file_size 0; | proxy_max_temp_file_size    0; | ||||||
| 
 | 
 | ||||||
| proxy_set_header Accept-Encoding ""; | proxy_set_header Accept-Encoding ""; | ||||||
| proxy_set_header Connection $connection_upgrade; | proxy_set_header Connection $connection_upgrade; | ||||||
| @@ -1,5 +1,7 @@ | |||||||
| root /dev/null; | root            /dev/null; | ||||||
| server_name $hostname; | server_name     $hostname; | ||||||
|  | 
 | ||||||
|  | client_max_body_size 512m; | ||||||
| 
 | 
 | ||||||
| add_header X-Content-Type-Options nosniff; | add_header X-Content-Type-Options nosniff; | ||||||
| add_header X-XSS-Protection "1; mode=block"; | 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; | user root; | ||||||
| pid /var/run/nginx.pid; | pid /var/run/nginx.pid; | ||||||
| worker_processes 1; | worker_processes 1; | ||||||
| # Hass.io addon log |  | ||||||
| error_log /proc/1/fd/1 error; | error_log /proc/1/fd/1 error; | ||||||
| events { | events { | ||||||
|     worker_connections 1024; |     worker_connections 1024; | ||||||
| @@ -10,24 +9,22 @@ events { | |||||||
| 
 | 
 | ||||||
| http { | http { | ||||||
|     include /etc/nginx/includes/mime.types; |     include /etc/nginx/includes/mime.types; | ||||||
|     access_log stdout; | 
 | ||||||
|     default_type application/octet-stream; |     access_log              off; | ||||||
|     gzip on; |     default_type            application/octet-stream; | ||||||
|     keepalive_timeout 65; |     gzip                    on; | ||||||
|     sendfile on; |     keepalive_timeout       65; | ||||||
|     server_tokens off; |     sendfile                on; | ||||||
|  |     server_tokens           off; | ||||||
|  | 
 | ||||||
|  |     tcp_nodelay             on; | ||||||
|  |     tcp_nopush              on; | ||||||
| 
 | 
 | ||||||
|     map $http_upgrade $connection_upgrade { |     map $http_upgrade $connection_upgrade { | ||||||
|         default upgrade; |         default upgrade; | ||||||
|         ''      close; |         ''      close; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     # Use Hass.io supervisor as resolver |     include /etc/nginx/includes/upstream.conf; | ||||||
|     resolver 172.30.32.2; |  | ||||||
| 
 |  | ||||||
|     upstream esphome { |  | ||||||
|         server unix:/var/run/esphome.sock; |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     include /etc/nginx/servers/*.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 { | 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/server_params.conf; | ||||||
|     include /etc/nginx/includes/proxy_params.conf; |     include /etc/nginx/includes/proxy_params.conf; | ||||||
|  | 
 | ||||||
|  |     {{ if .ssl }} | ||||||
|     include /etc/nginx/includes/ssl_params.conf; |     include /etc/nginx/includes/ssl_params.conf; | ||||||
| 
 | 
 | ||||||
|     ssl on; |     ssl_certificate /ssl/{{ .certfile }}; | ||||||
|     ssl_certificate /ssl/%%certfile%%; |     ssl_certificate_key /ssl/{{ .keyfile }}; | ||||||
|     ssl_certificate_key /ssl/%%keyfile%%; |  | ||||||
| 
 |  | ||||||
|     # Clear Hass.io Ingress header |  | ||||||
|     proxy_set_header X-Hassio-Ingress ""; |  | ||||||
| 
 | 
 | ||||||
|     # Redirect http requests to https on the same port. |     # Redirect http requests to https on the same port. | ||||||
|     # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ |     # https://rageagainstshell.com/2016/11/redirect-http-to-https-on-the-same-port-in-nginx/ | ||||||
|     error_page 497 https://$http_host$request_uri; |     error_page 497 https://$http_host$request_uri; | ||||||
|  |     {{ end }} | ||||||
|  | 
 | ||||||
|  |     # Clear Home Assistant Ingress header | ||||||
|  |     proxy_set_header X-HA-Ingress ""; | ||||||
| 
 | 
 | ||||||
|     location / { |     location / { | ||||||
|         proxy_pass http://esphome; |         proxy_pass http://esphome; | ||||||
| @@ -1,14 +1,16 @@ | |||||||
| server { | 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/server_params.conf; | ||||||
|     include /etc/nginx/includes/proxy_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 / { |     location / { | ||||||
|         # Only allow from Hass.io supervisor |  | ||||||
|         allow   172.30.32.2; |         allow   172.30.32.2; | ||||||
|  |         allow   127.0.0.1; | ||||||
|         deny    all; |         deny    all; | ||||||
| 
 | 
 | ||||||
|         proxy_pass http://esphome; |         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 | # Community Hass.io Add-ons: ESPHome | ||||||
| # Runs the ESPHome dashboard | # 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 | if bashio::config.true 'leave_front_door_open'; then | ||||||
|     export DISABLE_HA_AUTHENTICATION=true |     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') |     export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') | ||||||
| fi | fi | ||||||
| 
 | 
 | ||||||
| pio_cache_base=/data/cache/platformio | if bashio::config.has_value 'default_compile_process_limit'; then | ||||||
| # we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` |     export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=$(bashio::config 'default_compile_process_limit') | ||||||
| # setting `core_dir` would therefore prevent pio from accessing | else | ||||||
| export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" |     if grep -q 'Raspberry Pi 3' /proc/cpuinfo; then | ||||||
| export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" |         export ESPHOME_DEFAULT_COMPILE_PROCESS_LIMIT=1; | ||||||
| export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" |     fi | ||||||
|  | fi | ||||||
| 
 | 
 | ||||||
| export PLATFORMIO_GLOBALLIB_DIR=/piolibs | mkdir -p "${pio_cache_base}" | ||||||
| 
 | 
 | ||||||
| bashio::log.info "Starting ESPHome dashboard..." | 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 | # Community Hass.io Add-ons: ESPHome | ||||||
| # Runs the NGINX proxy | # 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 | while [[ ! -S /var/run/esphome.sock ]]; do | ||||||
|   sleep 0.5 |   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 functools | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
|  | import re | ||||||
| import sys | import sys | ||||||
|  | import time | ||||||
| from datetime import datetime | from datetime import datetime | ||||||
|  |  | ||||||
| from esphome import const, writer, yaml_util | from esphome import const, writer, yaml_util | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.config import iter_components, read_config, strip_default_ids | from esphome.config import iter_components, read_config, strip_default_ids | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|  |     ALLOWED_NAME_CHARS, | ||||||
|     CONF_BAUD_RATE, |     CONF_BAUD_RATE, | ||||||
|     CONF_BROKER, |     CONF_BROKER, | ||||||
|     CONF_DEASSERT_RTS_DTR, |     CONF_DEASSERT_RTS_DTR, | ||||||
|     CONF_LOGGER, |     CONF_LOGGER, | ||||||
|  |     CONF_NAME, | ||||||
|     CONF_OTA, |     CONF_OTA, | ||||||
|  |     CONF_MQTT, | ||||||
|  |     CONF_MDNS, | ||||||
|  |     CONF_DISABLED, | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|     CONF_PORT, |     CONF_PORT, | ||||||
|     CONF_ESPHOME, |     CONF_ESPHOME, | ||||||
|     CONF_PLATFORMIO_OPTIONS, |     CONF_PLATFORMIO_OPTIONS, | ||||||
|  |     CONF_SUBSTITUTIONS, | ||||||
|  |     PLATFORM_ESP32, | ||||||
|  |     PLATFORM_ESP8266, | ||||||
|  |     PLATFORM_RP2040, | ||||||
|     SECRETS_FILES, |     SECRETS_FILES, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, EsphomeError, coroutine | from esphome.core import CORE, EsphomeError, coroutine | ||||||
| @@ -34,7 +45,7 @@ from esphome.log import color, setup_log, Fore | |||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| def choose_prompt(options): | def choose_prompt(options, purpose: str = None): | ||||||
|     if not options: |     if not options: | ||||||
|         raise EsphomeError( |         raise EsphomeError( | ||||||
|             "Found no valid options for upload/logging, please make sure relevant " |             "Found no valid options for upload/logging, please make sure relevant " | ||||||
| @@ -45,7 +56,9 @@ def choose_prompt(options): | |||||||
|     if len(options) == 1: |     if len(options) == 1: | ||||||
|         return options[0][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): |     for i, (desc, _) in enumerate(options): | ||||||
|         safe_print(f"  [{i+1}] {desc}") |         safe_print(f"  [{i+1}] {desc}") | ||||||
|  |  | ||||||
| @@ -64,7 +77,9 @@ def choose_prompt(options): | |||||||
|     return options[opt - 1][1] |     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 = [] |     options = [] | ||||||
|     for port in get_serial_ports(): |     for port in get_serial_ports(): | ||||||
|         options.append((f"{port.path} ({port.description})", port.path)) |         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)) |         options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||||
|         if default == "OTA": |         if default == "OTA": | ||||||
|             return CORE.address |             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")) |         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) | ||||||
|         if default == "OTA": |         if default == "OTA": | ||||||
|             return "MQTT" |             return "MQTT" | ||||||
| @@ -80,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | |||||||
|         return default |         return default | ||||||
|     if check_default is not None and check_default in [opt[1] for opt in options]: |     if check_default is not None and check_default in [opt[1] for opt in options]: | ||||||
|         return check_default |         return check_default | ||||||
|     return choose_prompt(options) |     return choose_prompt(options, purpose=purpose) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_port_type(port): | def get_port_type(port): | ||||||
| @@ -97,11 +112,11 @@ def run_miniterm(config, port): | |||||||
|  |  | ||||||
|     if CONF_LOGGER not in config: |     if CONF_LOGGER not in config: | ||||||
|         _LOGGER.info("Logger is not enabled. Not starting UART logs.") |         _LOGGER.info("Logger is not enabled. Not starting UART logs.") | ||||||
|         return |         return 1 | ||||||
|     baud_rate = config["logger"][CONF_BAUD_RATE] |     baud_rate = config["logger"][CONF_BAUD_RATE] | ||||||
|     if baud_rate == 0: |     if baud_rate == 0: | ||||||
|         _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") |         _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) |     _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) | ||||||
|  |  | ||||||
|     backtrace_state = False |     backtrace_state = False | ||||||
| @@ -115,25 +130,36 @@ def run_miniterm(config, port): | |||||||
|         ser.dtr = False |         ser.dtr = False | ||||||
|         ser.rts = False |         ser.rts = False | ||||||
|  |  | ||||||
|     with ser: |     tries = 0 | ||||||
|         while True: |     while tries < 5: | ||||||
|             try: |         try: | ||||||
|                 raw = ser.readline() |             with ser: | ||||||
|             except serial.SerialException: |                 while True: | ||||||
|                 _LOGGER.error("Serial port closed!") |                     try: | ||||||
|                 return |                         raw = ser.readline() | ||||||
|             line = ( |                     except serial.SerialException: | ||||||
|                 raw.replace(b"\r", b"") |                         _LOGGER.error("Serial port closed!") | ||||||
|                 .replace(b"\n", b"") |                         return 0 | ||||||
|                 .decode("utf8", "backslashreplace") |                     line = ( | ||||||
|             ) |                         raw.replace(b"\r", b"") | ||||||
|             time = datetime.now().time().strftime("[%H:%M:%S]") |                         .replace(b"\n", b"") | ||||||
|             message = time + line |                         .decode("utf8", "backslashreplace") | ||||||
|             safe_print(message) |                     ) | ||||||
|  |                     time_str = datetime.now().time().strftime("[%H:%M:%S]") | ||||||
|  |                     message = time_str + line | ||||||
|  |                     safe_print(message) | ||||||
|  |  | ||||||
|             backtrace_state = platformio_api.process_stacktrace( |                     backtrace_state = platformio_api.process_stacktrace( | ||||||
|                 config, line, backtrace_state=backtrace_state |                         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): | 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: |         if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: | ||||||
|             import esptool |             import esptool | ||||||
|  |  | ||||||
|             # pylint: disable=protected-access |             return run_external_command(esptool.main, *cmd)  # pylint: disable=no-member | ||||||
|             return run_external_command(esptool._main, *cmd) |  | ||||||
|  |  | ||||||
|         return run_external_process(*cmd) |         return run_external_process(*cmd) | ||||||
|  |  | ||||||
| @@ -254,11 +279,21 @@ def upload_using_esptool(config, port): | |||||||
|  |  | ||||||
|  |  | ||||||
| def upload_program(config, args, host): | def upload_program(config, args, host): | ||||||
|     # if upload is to a serial port use platformio, otherwise assume ota |  | ||||||
|     if get_port_type(host) == "SERIAL": |     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: |     if CONF_OTA not in config: | ||||||
|         raise EsphomeError( |         raise EsphomeError( | ||||||
| @@ -266,9 +301,24 @@ def upload_program(config, args, host): | |||||||
|             "component" |             "component" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     from esphome import espota2 | ||||||
|  |  | ||||||
|     ota_conf = config[CONF_OTA] |     ota_conf = config[CONF_OTA] | ||||||
|     remote_port = ota_conf[CONF_PORT] |     remote_port = ota_conf[CONF_PORT] | ||||||
|     password = ota_conf.get(CONF_PASSWORD, "") |     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) |     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: |     if "logger" not in config: | ||||||
|         raise EsphomeError("Logger is not configured!") |         raise EsphomeError("Logger is not configured!") | ||||||
|     if get_port_type(port) == "SERIAL": |     if get_port_type(port) == "SERIAL": | ||||||
|         run_miniterm(config, port) |         return run_miniterm(config, port) | ||||||
|         return 0 |  | ||||||
|     if get_port_type(port) == "NETWORK" and "api" in config: |     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 |         from esphome.components.api.client import run_logs | ||||||
|  |  | ||||||
|         return run_logs(config, port) |         return run_logs(config, port) | ||||||
| @@ -310,7 +366,7 @@ def command_config(args, config): | |||||||
|     _LOGGER.info("Configuration is valid!") |     _LOGGER.info("Configuration is valid!") | ||||||
|     if not CORE.verbose: |     if not CORE.verbose: | ||||||
|         config = strip_default_ids(config) |         config = strip_default_ids(config) | ||||||
|     safe_print(yaml_util.dump(config)) |     safe_print(yaml_util.dump(config, args.show_secrets)) | ||||||
|     return 0 |     return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -343,6 +399,7 @@ def command_upload(args, config): | |||||||
|         show_ota=True, |         show_ota=True, | ||||||
|         show_mqtt=False, |         show_mqtt=False, | ||||||
|         show_api=False, |         show_api=False, | ||||||
|  |         purpose="uploading", | ||||||
|     ) |     ) | ||||||
|     exit_code = upload_program(config, args, port) |     exit_code = upload_program(config, args, port) | ||||||
|     if exit_code != 0: |     if exit_code != 0: | ||||||
| @@ -351,6 +408,15 @@ def command_upload(args, config): | |||||||
|     return 0 |     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): | def command_logs(args, config): | ||||||
|     port = choose_upload_log_host( |     port = choose_upload_log_host( | ||||||
|         default=args.device, |         default=args.device, | ||||||
| @@ -358,6 +424,7 @@ def command_logs(args, config): | |||||||
|         show_ota=False, |         show_ota=False, | ||||||
|         show_mqtt=True, |         show_mqtt=True, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="logging", | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, port) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
| @@ -376,6 +443,7 @@ def command_run(args, config): | |||||||
|         show_ota=True, |         show_ota=True, | ||||||
|         show_mqtt=False, |         show_mqtt=False, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="uploading", | ||||||
|     ) |     ) | ||||||
|     exit_code = upload_program(config, args, port) |     exit_code = upload_program(config, args, port) | ||||||
|     if exit_code != 0: |     if exit_code != 0: | ||||||
| @@ -389,6 +457,7 @@ def command_run(args, config): | |||||||
|         show_ota=False, |         show_ota=False, | ||||||
|         show_mqtt=True, |         show_mqtt=True, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="logging", | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, port) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
| @@ -481,6 +550,98 @@ def command_idedata(args, config): | |||||||
|     return 0 |     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 = { | PRE_CONFIG_ACTIONS = { | ||||||
|     "wizard": command_wizard, |     "wizard": command_wizard, | ||||||
|     "version": command_version, |     "version": command_version, | ||||||
| @@ -499,6 +660,8 @@ POST_CONFIG_ACTIONS = { | |||||||
|     "mqtt-fingerprint": command_mqtt_fingerprint, |     "mqtt-fingerprint": command_mqtt_fingerprint, | ||||||
|     "clean": command_clean, |     "clean": command_clean, | ||||||
|     "idedata": command_idedata, |     "idedata": command_idedata, | ||||||
|  |     "rename": command_rename, | ||||||
|  |     "discover": command_discover, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -543,6 +706,9 @@ def parse_args(argv): | |||||||
|     parser_config.add_argument( |     parser_config.add_argument( | ||||||
|         "configuration", help="Your YAML configuration file(s).", nargs="+" |         "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( |     parser_compile = subparsers.add_parser( | ||||||
|         "compile", help="Read the configuration and compile a program." |         "compile", help="Read the configuration and compile a program." | ||||||
| @@ -566,6 +732,10 @@ def parse_args(argv): | |||||||
|         "--device", |         "--device", | ||||||
|         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", |         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( |     parser_logs = subparsers.add_parser( | ||||||
|         "logs", |         "logs", | ||||||
| @@ -580,6 +750,15 @@ def parse_args(argv): | |||||||
|         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", |         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( |     parser_run = subparsers.add_parser( | ||||||
|         "run", |         "run", | ||||||
|         help="Validate the configuration, create a binary, upload it, and start logs.", |         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" |         "--open-ui", help="Open the dashboard UI in a browser.", action="store_true" | ||||||
|     ) |     ) | ||||||
|     parser_dashboard.add_argument( |     parser_dashboard.add_argument( | ||||||
|         "--hassio", help=argparse.SUPPRESS, action="store_true" |         "--ha-addon", help=argparse.SUPPRESS, action="store_true" | ||||||
|     ) |     ) | ||||||
|     parser_dashboard.add_argument( |     parser_dashboard.add_argument( | ||||||
|         "--socket", help="Make the dashboard serve under a unix socket", type=str |         "--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 |         "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 |     # Keep backward compatibility with the old command line format of | ||||||
|     # esphome <config> <command>. |     # esphome <config> <command>. | ||||||
|     # |     # | ||||||
| @@ -778,10 +966,10 @@ def run_esphome(argv): | |||||||
|         _LOGGER.warning("Please instead use:") |         _LOGGER.warning("Please instead use:") | ||||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion)) |         _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( |         _LOGGER.error( | ||||||
|             "You're running ESPHome with Python <3.7. ESPHome is no longer compatible " |             "You're running ESPHome with Python <3.8. ESPHome is no longer compatible " | ||||||
|             "with this Python version. Please reinstall ESPHome with Python 3.7+" |             "with this Python version. Please reinstall ESPHome with Python 3.8+" | ||||||
|         ) |         ) | ||||||
|         return 1 |         return 1 | ||||||
|  |  | ||||||
| @@ -792,6 +980,8 @@ def run_esphome(argv): | |||||||
|             _LOGGER.error(e, exc_info=args.verbose) |             _LOGGER.error(e, exc_info=args.verbose) | ||||||
|             return 1 |             return 1 | ||||||
|  |  | ||||||
|  |     _LOGGER.info("ESPHome %s", const.__version__) | ||||||
|  |  | ||||||
|     for conf_path in args.configuration: |     for conf_path in args.configuration: | ||||||
|         if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): |         if any(os.path.basename(conf_path) == x for x in SECRETS_FILES): | ||||||
|             _LOGGER.warning("Skipping secrets file %s", conf_path) |             _LOGGER.warning("Skipping secrets file %s", conf_path) | ||||||
|   | |||||||
| @@ -12,7 +12,7 @@ from esphome.const import ( | |||||||
|     CONF_TYPE_ID, |     CONF_TYPE_ID, | ||||||
|     CONF_TIME, |     CONF_TIME, | ||||||
| ) | ) | ||||||
| from esphome.jsonschema import jschema_extractor | from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||||
| from esphome.util import Registry | from esphome.util import Registry | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -23,11 +23,10 @@ def maybe_simple_id(*validators): | |||||||
| def maybe_conf(conf, *validators): | def maybe_conf(conf, *validators): | ||||||
|     validator = cv.All(*validators) |     validator = cv.All(*validators) | ||||||
|  |  | ||||||
|     @jschema_extractor("maybe") |     @schema_extractor("maybe") | ||||||
|     def validate(value): |     def validate(value): | ||||||
|         # pylint: disable=comparison-with-callable |         if value == SCHEMA_EXTRACT: | ||||||
|         if value == jschema_extractor: |             return (validator, conf) | ||||||
|             return validator |  | ||||||
|  |  | ||||||
|         if isinstance(value, dict): |         if isinstance(value, dict): | ||||||
|             return validator(value) |             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. |         # This should only happen with invalid configs, but let's have a nice error message. | ||||||
|         return [schema(value)] |         return [schema(value)] | ||||||
|  |  | ||||||
|     @jschema_extractor("automation") |     @schema_extractor("automation") | ||||||
|     def validator(value): |     def validator(value): | ||||||
|         # hack to get the schema |         if value == SCHEMA_EXTRACT: | ||||||
|         # pylint: disable=comparison-with-callable |  | ||||||
|         if value == jschema_extractor: |  | ||||||
|             return schema |             return schema | ||||||
|  |  | ||||||
|         value = validator_(value) |         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) |     var = cg.new_Pvariable(action_id, template_arg) | ||||||
|     count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) |     count_template = await cg.templatable(config[CONF_COUNT], args, cg.uint32) | ||||||
|     cg.add(var.set_count(count_template)) |     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)) |     cg.add(var.add_then(actions)) | ||||||
|     return var |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_wait_until(value): | _validate_wait_until = cv.maybe_simple_value( | ||||||
|     schema = cv.Schema( |     { | ||||||
|         { |         cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||||
|             cv.Required(CONF_CONDITION): validate_potentially_and_condition, |         cv.Optional(CONF_TIMEOUT): cv.templatable(cv.positive_time_period_milliseconds), | ||||||
|             cv.Optional(CONF_TIMEOUT): cv.templatable( |     }, | ||||||
|                 cv.positive_time_period_milliseconds |     key=CONF_CONDITION, | ||||||
|             ), | ) | ||||||
|         } |  | ||||||
|     ) |  | ||||||
|     if isinstance(value, dict) and CONF_CONDITION in value: |  | ||||||
|         return schema(value) |  | ||||||
|     return validate_wait_until({CONF_CONDITION: value}) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| @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): | async def wait_until_action_to_code(config, action_id, template_arg, args): | ||||||
|     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) |     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) | ||||||
|     var = cg.new_Pvariable(action_id, template_arg, conditions) |     var = cg.new_Pvariable(action_id, template_arg, conditions) | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ from esphome.cpp_generator import (  # noqa | |||||||
|     static_const_array, |     static_const_array, | ||||||
|     statement, |     statement, | ||||||
|     variable, |     variable, | ||||||
|  |     with_local_variable, | ||||||
|     new_variable, |     new_variable, | ||||||
|     Pvariable, |     Pvariable, | ||||||
|     new_Pvariable, |     new_Pvariable, | ||||||
| @@ -46,6 +47,7 @@ from esphome.cpp_helpers import (  # noqa | |||||||
|     build_registry_list, |     build_registry_list, | ||||||
|     extract_registry_entry_config, |     extract_registry_entry_config, | ||||||
|     register_parented, |     register_parented, | ||||||
|  |     past_safe_mode, | ||||||
| ) | ) | ||||||
| from esphome.cpp_types import (  # noqa | from esphome.cpp_types import (  # noqa | ||||||
|     global_ns, |     global_ns, | ||||||
| @@ -62,7 +64,10 @@ from esphome.cpp_types import (  # noqa | |||||||
|     uint16, |     uint16, | ||||||
|     uint32, |     uint32, | ||||||
|     uint64, |     uint64, | ||||||
|  |     int16, | ||||||
|     int32, |     int32, | ||||||
|  |     int64, | ||||||
|  |     size_t, | ||||||
|     const_char_ptr, |     const_char_ptr, | ||||||
|     NAN, |     NAN, | ||||||
|     esphome_ns, |     esphome_ns, | ||||||
| @@ -81,4 +86,5 @@ from esphome.cpp_types import (  # noqa | |||||||
|     InternalGPIOPin, |     InternalGPIOPin, | ||||||
|     gpio_Flags, |     gpio_Flags, | ||||||
|     EntityCategory, |     EntityCategory, | ||||||
|  |     Parented, | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ void A4988::loop() { | |||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   this->dir_pin_->digital_write(dir == 1); |   this->dir_pin_->digital_write(dir == 1); | ||||||
|  |   delayMicroseconds(50); | ||||||
|   this->step_pin_->digital_write(true); |   this->step_pin_->digital_write(true); | ||||||
|   delayMicroseconds(5); |   delayMicroseconds(5); | ||||||
|   this->step_pin_->digital_write(false); |   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 |       // also take into account min_power | ||||||
|       auto min_us = this->cycle_time_us * this->min_power / 1000; |       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); |       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) { |       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 |         // 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% |         // this is for brightness near 99% | ||||||
| @@ -202,6 +203,7 @@ void AcDimmer::setup() { | |||||||
| #endif | #endif | ||||||
| } | } | ||||||
| void AcDimmer::write_state(float state) { | 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)); |   auto new_value = static_cast<uint16_t>(roundf(state * 65535)); | ||||||
|   if (new_value != 0 && this->store_.value == 0) |   if (new_value != 0 && this->store_.value == 0) | ||||||
|     this->store_.init_cycle = this->init_with_half_cycle_; |     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"] | 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 | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_RP2040 | ||||||
|  | #include <hardware/adc.h> | ||||||
|  | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace adc { | namespace adc { | ||||||
|  |  | ||||||
| static const char *const TAG = "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 | #ifdef USE_ESP32 | ||||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | 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 | #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()); |   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(); |   pin_->setup(); | ||||||
| #endif | #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 | #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() { | void ADCSensor::dump_config() { | ||||||
| @@ -75,22 +100,29 @@ void ADCSensor::dump_config() { | |||||||
|   } else { |   } else { | ||||||
|     switch (this->attenuation_) { |     switch (this->attenuation_) { | ||||||
|       case ADC_ATTEN_DB_0: |       case ADC_ATTEN_DB_0: | ||||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); |         ESP_LOGCONFIG(TAG, " Attenuation: 0db"); | ||||||
|         break; |         break; | ||||||
|       case ADC_ATTEN_DB_2_5: |       case ADC_ATTEN_DB_2_5: | ||||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); |         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); | ||||||
|         break; |         break; | ||||||
|       case ADC_ATTEN_DB_6: |       case ADC_ATTEN_DB_6: | ||||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); |         ESP_LOGCONFIG(TAG, " Attenuation: 6db"); | ||||||
|         break; |         break; | ||||||
|       case ADC_ATTEN_DB_11: |       case ADC_ATTEN_DB_11: | ||||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); |         ESP_LOGCONFIG(TAG, " Attenuation: 11db"); | ||||||
|         break; |         break; | ||||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX |       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| #endif  // USE_ESP32 | #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); |   LOG_UPDATE_INTERVAL(this); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -129,16 +161,16 @@ float ADCSensor::sample() { | |||||||
|     return mv / 1000.0f; |     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); |   adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); | ||||||
|   raw11 = adc1_get_raw(channel_); |   raw11 = adc1_get_raw(channel_); | ||||||
|   if (raw11 < 4095) { |   if (raw11 < ADC_MAX) { | ||||||
|     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); |     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); | ||||||
|     raw6 = adc1_get_raw(channel_); |     raw6 = adc1_get_raw(channel_); | ||||||
|     if (raw6 < 4095) { |     if (raw6 < ADC_MAX) { | ||||||
|       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); |       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); | ||||||
|       raw2 = adc1_get_raw(channel_); |       raw2 = adc1_get_raw(channel_); | ||||||
|       if (raw2 < 4095) { |       if (raw2 < ADC_MAX) { | ||||||
|         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); |         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); | ||||||
|         raw0 = adc1_get_raw(channel_); |         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 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]); |   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 |   // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) | ||||||
|   uint32_t c11 = std::min(raw11, 2048); |   uint32_t c11 = std::min(raw11, ADC_HALF); | ||||||
|   uint32_t c6 = 2048 - std::abs(raw6 - 2048); |   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||||
|   uint32_t c2 = 2048 - std::abs(raw2 - 2048); |   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||||
|   uint32_t c0 = std::min(4095 - raw0, 2048); |   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||||
|   // max theoretical csum value is 2048*4 = 8192 |   // max theoretical csum value is 4096*4 = 16384 | ||||||
|   uint32_t csum = c11 + c6 + c2 + c0; |   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); |   uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||||
|   return mv_scaled / (float) (csum * 1000U); |   return mv_scaled / (float) (csum * 1000U); | ||||||
| } | } | ||||||
| #endif  // USE_ESP32 | #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 | #ifdef USE_ESP8266 | ||||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -38,10 +38,18 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | |||||||
|   std::string unique_id() override; |   std::string unique_id() override; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_RP2040 | ||||||
|  |   void set_is_temperature() { is_temperature_ = true; } | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   InternalGPIOPin *pin_; |   InternalGPIOPin *pin_; | ||||||
|   bool output_raw_{false}; |   bool output_raw_{false}; | ||||||
|  |  | ||||||
|  | #ifdef USE_RP2040 | ||||||
|  |   bool is_temperature_{false}; | ||||||
|  | #endif | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; |   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||||
|   adc1_channel_t channel_{}; |   adc1_channel_t channel_{}; | ||||||
|   | |||||||
| @@ -1,124 +1,27 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome import pins |  | ||||||
| from esphome.components import sensor, voltage_sampler | from esphome.components import sensor, voltage_sampler | ||||||
|  | from esphome.components.esp32 import get_esp32_variant | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_ATTENUATION, |     CONF_ATTENUATION, | ||||||
|     CONF_RAW, |  | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_INPUT, |  | ||||||
|     CONF_NUMBER, |     CONF_NUMBER, | ||||||
|     CONF_PIN, |     CONF_PIN, | ||||||
|  |     CONF_RAW, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE | from esphome.core import CORE | ||||||
| from esphome.components.esp32 import get_esp32_variant |  | ||||||
| from esphome.components.esp32.const import ( | from . import ( | ||||||
|     VARIANT_ESP32, |     ATTENUATION_MODES, | ||||||
|     VARIANT_ESP32C3, |     ESP32_VARIANT_ADC1_PIN_TO_CHANNEL, | ||||||
|     VARIANT_ESP32H2, |     validate_adc_pin, | ||||||
|     VARIANT_ESP32S2, |  | ||||||
|     VARIANT_ESP32S3, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| AUTO_LOAD = ["voltage_sampler"] | 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): | def validate_config(config): | ||||||
|     if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": |     if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto": | ||||||
| @@ -133,6 +36,7 @@ ADCSensor = adc_ns.class_( | |||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.All( | ||||||
|     sensor.sensor_schema( |     sensor.sensor_schema( | ||||||
|  |         ADCSensor, | ||||||
|         unit_of_measurement=UNIT_VOLT, |         unit_of_measurement=UNIT_VOLT, | ||||||
|         accuracy_decimals=2, |         accuracy_decimals=2, | ||||||
|         device_class=DEVICE_CLASS_VOLTAGE, |         device_class=DEVICE_CLASS_VOLTAGE, | ||||||
| @@ -140,7 +44,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|     ) |     ) | ||||||
|     .extend( |     .extend( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(ADCSensor), |  | ||||||
|             cv.Required(CONF_PIN): validate_adc_pin, |             cv.Required(CONF_PIN): validate_adc_pin, | ||||||
|             cv.Optional(CONF_RAW, default=False): cv.boolean, |             cv.Optional(CONF_RAW, default=False): cv.boolean, | ||||||
|             cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( |             cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All( | ||||||
| @@ -160,6 +63,8 @@ async def to_code(config): | |||||||
|  |  | ||||||
|     if config[CONF_PIN] == "VCC": |     if config[CONF_PIN] == "VCC": | ||||||
|         cg.add_define("USE_ADC_SENSOR_VCC") |         cg.add_define("USE_ADC_SENSOR_VCC") | ||||||
|  |     elif config[CONF_PIN] == "TEMPERATURE": | ||||||
|  |         cg.add(var.set_is_temperature()) | ||||||
|     else: |     else: | ||||||
|         pin = await cg.gpio_pin_expression(config[CONF_PIN]) |         pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||||
|         cg.add(var.set_pin(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/display/display_buffer.h" | ||||||
| #include "esphome/components/light/addressable_light.h" | #include "esphome/components/light/addressable_light.h" | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace addressable_light { | namespace addressable_light { | ||||||
|  |  | ||||||
| @@ -40,6 +42,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom | |||||||
|   void setup() override; |   void setup() override; | ||||||
|   void display(); |   void display(); | ||||||
|  |  | ||||||
|  |   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   int get_width_internal() override; |   int get_width_internal() override; | ||||||
|   int get_height_internal() override; |   int get_height_internal() override; | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ | |||||||
| #include "esphome/components/i2c/i2c.h" | #include "esphome/components/i2c/i2c.h" | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ade7953 { | namespace ade7953 { | ||||||
|  |  | ||||||
| @@ -82,7 +84,7 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { | |||||||
|     return i2c::ERROR_OK; |     return i2c::ERROR_OK; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   InternalGPIOPin *irq_pin_ = nullptr; |   InternalGPIOPin *irq_pin_{nullptr}; | ||||||
|   bool is_setup_{false}; |   bool is_setup_{false}; | ||||||
|   sensor::Sensor *voltage_sensor_{nullptr}; |   sensor::Sensor *voltage_sensor_{nullptr}; | ||||||
|   sensor::Sensor *current_a_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_CONVERSION = 0x00; | ||||||
| static const uint8_t ADS1115_REGISTER_CONFIG = 0x01; | 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() { | void ADS1115Component::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); |   ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); | ||||||
| @@ -18,6 +18,9 @@ void ADS1115Component::setup() { | |||||||
|     this->mark_failed(); |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "Configuring ADS1115..."); | ||||||
|  |  | ||||||
|   uint16_t config = 0; |   uint16_t config = 0; | ||||||
|   // Clear single-shot bit |   // Clear single-shot bit | ||||||
|   //        0b0xxxxxxxxxxxxxxx |   //        0b0xxxxxxxxxxxxxxx | ||||||
| @@ -77,6 +80,7 @@ void ADS1115Component::dump_config() { | |||||||
|     LOG_SENSOR("  ", "Sensor", sensor); |     LOG_SENSOR("  ", "Sensor", sensor); | ||||||
|     ESP_LOGCONFIG(TAG, "    Multiplexer: %u", sensor->get_multiplexer()); |     ESP_LOGCONFIG(TAG, "    Multiplexer: %u", sensor->get_multiplexer()); | ||||||
|     ESP_LOGCONFIG(TAG, "    Gain: %u", sensor->get_gain()); |     ESP_LOGCONFIG(TAG, "    Gain: %u", sensor->get_gain()); | ||||||
|  |     ESP_LOGCONFIG(TAG, "    Resolution: %u", sensor->get_resolution()); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | ||||||
| @@ -127,27 +131,45 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | |||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     return NAN; |     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); |   auto signed_conversion = static_cast<int16_t>(raw_conversion); | ||||||
|  |  | ||||||
|   float millivolts; |   float millivolts; | ||||||
|  |   float divider = (sensor->get_resolution() == ADS1115_16_BITS) ? 32768.0f : 2048.0f; | ||||||
|   switch (sensor->get_gain()) { |   switch (sensor->get_gain()) { | ||||||
|     case ADS1115_GAIN_6P144: |     case ADS1115_GAIN_6P144: | ||||||
|       millivolts = signed_conversion * 0.187500f; |       millivolts = (signed_conversion * 6144) / divider; | ||||||
|       break; |       break; | ||||||
|     case ADS1115_GAIN_4P096: |     case ADS1115_GAIN_4P096: | ||||||
|       millivolts = signed_conversion * 0.125000f; |       millivolts = (signed_conversion * 4096) / divider; | ||||||
|       break; |       break; | ||||||
|     case ADS1115_GAIN_2P048: |     case ADS1115_GAIN_2P048: | ||||||
|       millivolts = signed_conversion * 0.062500f; |       millivolts = (signed_conversion * 2048) / divider; | ||||||
|       break; |       break; | ||||||
|     case ADS1115_GAIN_1P024: |     case ADS1115_GAIN_1P024: | ||||||
|       millivolts = signed_conversion * 0.031250f; |       millivolts = (signed_conversion * 1024) / divider; | ||||||
|       break; |       break; | ||||||
|     case ADS1115_GAIN_0P512: |     case ADS1115_GAIN_0P512: | ||||||
|       millivolts = signed_conversion * 0.015625f; |       millivolts = (signed_conversion * 512) / divider; | ||||||
|       break; |       break; | ||||||
|     case ADS1115_GAIN_0P256: |     case ADS1115_GAIN_0P256: | ||||||
|       millivolts = signed_conversion * 0.007813f; |       millivolts = (signed_conversion * 256) / divider; | ||||||
|       break; |       break; | ||||||
|     default: |     default: | ||||||
|       millivolts = NAN; |       millivolts = NAN; | ||||||
|   | |||||||
| @@ -5,6 +5,8 @@ | |||||||
| #include "esphome/components/i2c/i2c.h" | #include "esphome/components/i2c/i2c.h" | ||||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||||
|  |  | ||||||
|  | #include <vector> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ads1115 { | namespace ads1115 { | ||||||
|  |  | ||||||
| @@ -28,6 +30,11 @@ enum ADS1115Gain { | |||||||
|   ADS1115_GAIN_0P256 = 0b101, |   ADS1115_GAIN_0P256 = 0b101, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | enum ADS1115Resolution { | ||||||
|  |   ADS1115_16_BITS = 16, | ||||||
|  |   ADS1015_12_BITS = 12, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class ADS1115Sensor; | class ADS1115Sensor; | ||||||
|  |  | ||||||
| class ADS1115Component : public Component, public i2c::I2CDevice { | class ADS1115Component : public Component, public i2c::I2CDevice { | ||||||
| @@ -56,15 +63,17 @@ class ADS1115Sensor : public sensor::Sensor, public PollingComponent, public vol | |||||||
|   void update() override; |   void update() override; | ||||||
|   void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } |   void set_multiplexer(ADS1115Multiplexer multiplexer) { multiplexer_ = multiplexer; } | ||||||
|   void set_gain(ADS1115Gain gain) { gain_ = gain; } |   void set_gain(ADS1115Gain gain) { gain_ = gain; } | ||||||
|  |   void set_resolution(ADS1115Resolution resolution) { resolution_ = resolution; } | ||||||
|   float sample() override; |   float sample() override; | ||||||
|   uint8_t get_multiplexer() const { return multiplexer_; } |   uint8_t get_multiplexer() const { return multiplexer_; } | ||||||
|   uint8_t get_gain() const { return gain_; } |   uint8_t get_gain() const { return gain_; } | ||||||
|  |   uint8_t get_resolution() const { return resolution_; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   ADS1115Component *parent_; |   ADS1115Component *parent_; | ||||||
|   ADS1115Multiplexer multiplexer_; |   ADS1115Multiplexer multiplexer_; | ||||||
|   ADS1115Gain gain_; |   ADS1115Gain gain_; | ||||||
|  |   ADS1115Resolution resolution_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace ads1115 | }  // namespace ads1115 | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from esphome.components import sensor, voltage_sampler | |||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_GAIN, |     CONF_GAIN, | ||||||
|     CONF_MULTIPLEXER, |     CONF_MULTIPLEXER, | ||||||
|  |     CONF_RESOLUTION, | ||||||
|     DEVICE_CLASS_VOLTAGE, |     DEVICE_CLASS_VOLTAGE, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_VOLT, |     UNIT_VOLT, | ||||||
| @@ -35,6 +36,12 @@ GAIN = { | |||||||
|     "0.256": ADS1115Gain.ADS1115_GAIN_0P256, |     "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): | def validate_gain(value): | ||||||
|     if isinstance(value, float): |     if isinstance(value, float): | ||||||
| @@ -52,6 +59,7 @@ ADS1115Sensor = ads1115_ns.class_( | |||||||
| CONF_ADS1115_ID = "ads1115_id" | CONF_ADS1115_ID = "ads1115_id" | ||||||
| CONFIG_SCHEMA = ( | CONFIG_SCHEMA = ( | ||||||
|     sensor.sensor_schema( |     sensor.sensor_schema( | ||||||
|  |         ADS1115Sensor, | ||||||
|         unit_of_measurement=UNIT_VOLT, |         unit_of_measurement=UNIT_VOLT, | ||||||
|         accuracy_decimals=3, |         accuracy_decimals=3, | ||||||
|         device_class=DEVICE_CLASS_VOLTAGE, |         device_class=DEVICE_CLASS_VOLTAGE, | ||||||
| @@ -59,10 +67,12 @@ CONFIG_SCHEMA = ( | |||||||
|     ) |     ) | ||||||
|     .extend( |     .extend( | ||||||
|         { |         { | ||||||
|             cv.GenerateID(): cv.declare_id(ADS1115Sensor), |  | ||||||
|             cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), |             cv.GenerateID(CONF_ADS1115_ID): cv.use_id(ADS1115Component), | ||||||
|             cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), |             cv.Required(CONF_MULTIPLEXER): cv.enum(MUX, upper=True, space="_"), | ||||||
|             cv.Required(CONF_GAIN): validate_gain, |             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")) |     .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_multiplexer(config[CONF_MULTIPLEXER])) | ||||||
|     cg.add(var.set_gain(config[CONF_GAIN])) |     cg.add(var.set_gain(config[CONF_GAIN])) | ||||||
|  |     cg.add(var.set_resolution(config[CONF_RESOLUTION])) | ||||||
|  |  | ||||||
|     cg.add(paren.register_sensor(var)) |     cg.add(paren.register_sensor(var)) | ||||||
|   | |||||||
| @@ -122,8 +122,9 @@ void AHT10Component::update() { | |||||||
|     this->temperature_sensor_->publish_state(temperature); |     this->temperature_sensor_->publish_state(temperature); | ||||||
|   } |   } | ||||||
|   if (this->humidity_sensor_ != nullptr) { |   if (this->humidity_sensor_ != nullptr) { | ||||||
|     if (std::isnan(humidity)) |     if (std::isnan(humidity)) { | ||||||
|       ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); |       ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); | ||||||
|  |     } | ||||||
|     this->humidity_sensor_->publish_state(humidity); |     this->humidity_sensor_->publish_state(humidity); | ||||||
|   } |   } | ||||||
|   this->status_clear_warning(); |   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; } |   void set_humidity_sensor(sensor::Sensor *humidity_sensor) { humidity_sensor_ = humidity_sensor; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   sensor::Sensor *temperature_sensor_; |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|   sensor::Sensor *humidity_sensor_; |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace aht10 | }  // 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"; | 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, | void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||||
|                                             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) { |  | ||||||
|   auto *value = (WaveMiniReadings *) raw_value; |   auto *value = (WaveMiniReadings *) raw_value; | ||||||
|  |  | ||||||
|   if (sizeof(WaveMiniReadings) <= value_len) { |   if (sizeof(WaveMiniReadings) <= value_len) { | ||||||
|     this->humidity_sensor_->publish_state(value->humidity / 100.0f); |     if (this->humidity_sensor_ != nullptr) { | ||||||
|     this->pressure_sensor_->publish_state(value->pressure / 50.0f); |       this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||||
|     this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); |     } | ||||||
|     if (is_valid_voc_value_(value->voc)) { |  | ||||||
|  |     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->tvoc_sensor_->publish_state(value->voc); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // This instance must not stay connected |     // This instance must not stay connected | ||||||
|     // so other clients can connect to it (e.g. the |     // so other clients can connect to it (e.g. the | ||||||
|     // mobile app). |     // mobile app). | ||||||
|     parent()->set_enabled(false); |     this->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); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void AirthingsWaveMini::dump_config() { | 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("  ", "Humidity", this->humidity_sensor_); | ||||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); |   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); |   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||||
| } | } | ||||||
|  |  | ||||||
| AirthingsWaveMini::AirthingsWaveMini() | AirthingsWaveMini::AirthingsWaveMini() { | ||||||
|     : PollingComponent(10000), |   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), |   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | } | ||||||
|  |  | ||||||
| }  // namespace airthings_wave_mini | }  // namespace airthings_wave_mini | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,14 +2,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #include <esp_gattc_api.h> | #include "esphome/components/airthings_wave_base/airthings_wave_base.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 esphome { | ||||||
| namespace airthings_wave_mini { | 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 SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; | ||||||
| static const char *const CHARACTERISTIC_UUID = "b42e3b98-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: |  public: | ||||||
|   AirthingsWaveMini(); |   AirthingsWaveMini(); | ||||||
|  |  | ||||||
|   void dump_config() override; |   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: |  protected: | ||||||
|   bool is_valid_voc_value_(uint16_t voc); |   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||||
|  |  | ||||||
|   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_; |  | ||||||
|  |  | ||||||
|   struct WaveMiniReadings { |   struct WaveMiniReadings { | ||||||
|     uint16_t unused01; |     uint16_t unused01; | ||||||
|   | |||||||
| @@ -1,82 +1,28 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import sensor, ble_client | from esphome.components import airthings_wave_base | ||||||
|  |  | ||||||
| from esphome.const import ( | 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_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") | airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") | ||||||
| AirthingsWaveMini = airthings_wave_mini_ns.class_( | AirthingsWaveMini = airthings_wave_mini_ns.class_( | ||||||
|     "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode |     "AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||||
|     cv.Schema( |     { | ||||||
|         { |         cv.GenerateID(): cv.declare_id(AirthingsWaveMini), | ||||||
|             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), |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     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_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)) |  | ||||||
|   | |||||||
| @@ -7,55 +7,7 @@ namespace airthings_wave_plus { | |||||||
|  |  | ||||||
| static const char *const TAG = "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, | void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||||
|                                             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) { |  | ||||||
|   auto *value = (WavePlusReadings *) raw_value; |   auto *value = (WavePlusReadings *) raw_value; | ||||||
|  |  | ||||||
|   if (sizeof(WavePlusReadings) <= value_len) { |   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) { |     if (value->version == 1) { | ||||||
|       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); |       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||||
|  |  | ||||||
|       this->humidity_sensor_->publish_state(value->humidity / 2.0f); |       if (this->humidity_sensor_ != nullptr) { | ||||||
|       if (is_valid_radon_value_(value->radon)) { |         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); |         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->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 (this->temperature_sensor_ != nullptr) { | ||||||
|       if (is_valid_co2_value_(value->co2)) { |         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); |         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->tvoc_sensor_->publish_state(value->voc); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // This instance must not stay connected |       // This instance must not stay connected | ||||||
|       // so other clients can connect to it (e.g. the |       // so other clients can connect to it (e.g. the | ||||||
|       // mobile app). |       // mobile app). | ||||||
|       parent()->set_enabled(false); |       this->parent()->set_enabled(false); | ||||||
|     } else { |     } else { | ||||||
|       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); |       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_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; } | 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() { | 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("  ", "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("  ", "Temperature", this->temperature_sensor_); | ||||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); |   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); |  | ||||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_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() | AirthingsWavePlus::AirthingsWavePlus() { | ||||||
|     : PollingComponent(10000), |   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), |   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | } | ||||||
|  |  | ||||||
| }  // namespace airthings_wave_plus | }  // namespace airthings_wave_plus | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,14 +2,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #include <esp_gattc_api.h> | #include "esphome/components/airthings_wave_base/airthings_wave_base.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 esphome { | ||||||
| namespace airthings_wave_plus { | 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 SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | ||||||
| static const char *const CHARACTERISTIC_UUID = "b42e2a68-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: |  public: | ||||||
|   AirthingsWavePlus(); |   AirthingsWavePlus(); | ||||||
|  |  | ||||||
|   void dump_config() override; |   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(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_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_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool is_valid_radon_value_(uint16_t radon); |   bool is_valid_radon_value_(uint16_t radon); | ||||||
|   bool is_valid_voc_value_(uint16_t voc); |  | ||||||
|   bool is_valid_co2_value_(uint16_t co2); |   bool is_valid_co2_value_(uint16_t co2); | ||||||
|  |  | ||||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); |   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||||
|   void request_read_values_(); |  | ||||||
|  |  | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *radon_sensor_{nullptr}; |   sensor::Sensor *radon_sensor_{nullptr}; | ||||||
|   sensor::Sensor *radon_long_term_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 *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 { |   struct WavePlusReadings { | ||||||
|     uint8_t version; |     uint8_t version; | ||||||
|   | |||||||
| @@ -1,116 +1,64 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | 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 ( | from esphome.const import ( | ||||||
|     DEVICE_CLASS_CARBON_DIOXIDE, |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     DEVICE_CLASS_HUMIDITY, |  | ||||||
|     DEVICE_CLASS_TEMPERATURE, |  | ||||||
|     DEVICE_CLASS_PRESSURE, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_PERCENT, |  | ||||||
|     UNIT_CELSIUS, |  | ||||||
|     UNIT_HECTOPASCAL, |  | ||||||
|     ICON_RADIOACTIVE, |     ICON_RADIOACTIVE, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_RADON, |     CONF_RADON, | ||||||
|     CONF_RADON_LONG_TERM, |     CONF_RADON_LONG_TERM, | ||||||
|     CONF_HUMIDITY, |  | ||||||
|     CONF_TVOC, |  | ||||||
|     CONF_CO2, |     CONF_CO2, | ||||||
|     CONF_PRESSURE, |  | ||||||
|     CONF_TEMPERATURE, |  | ||||||
|     UNIT_BECQUEREL_PER_CUBIC_METER, |     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|     UNIT_PARTS_PER_MILLION, |     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") | airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | ||||||
| AirthingsWavePlus = airthings_wave_plus_ns.class_( | AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||||
|     "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode |     "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||||
|     cv.Schema( |     { | ||||||
|         { |         cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||||
|             cv.GenerateID(): cv.declare_id(AirthingsWavePlus), |         cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( |             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|                 unit_of_measurement=UNIT_PERCENT, |             icon=ICON_RADIOACTIVE, | ||||||
|                 device_class=DEVICE_CLASS_HUMIDITY, |             accuracy_decimals=0, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|                 accuracy_decimals=0, |         ), | ||||||
|             ), |         cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||||
|             cv.Optional(CONF_RADON): sensor.sensor_schema( |             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, |             icon=ICON_RADIOACTIVE, | ||||||
|                 icon=ICON_RADIOACTIVE, |             accuracy_decimals=0, | ||||||
|                 accuracy_decimals=0, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |         ), | ||||||
|             ), |         cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||||
|             cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( |             unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, |             accuracy_decimals=0, | ||||||
|                 icon=ICON_RADIOACTIVE, |             device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|                 accuracy_decimals=0, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|                 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), |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     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: |     if CONF_RADON in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_RADON]) |         sens = await sensor.new_sensor(config[CONF_RADON]) | ||||||
|         cg.add(var.set_radon(sens)) |         cg.add(var.set_radon(sens)) | ||||||
|     if CONF_RADON_LONG_TERM in config: |     if CONF_RADON_LONG_TERM in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) |         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) | ||||||
|         cg.add(var.set_radon_long_term(sens)) |         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: |     if CONF_CO2 in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_CO2]) |         sens = await sensor.new_sensor(config[CONF_CO2]) | ||||||
|         cg.add(var.set_co2(sens)) |         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