mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'dev' into ble-server-controller
This commit is contained in:
		
							
								
								
									
										33
									
								
								.clang-tidy
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								.clang-tidy
									
									
									
									
									
								
							| @@ -2,9 +2,11 @@ | ||||
| Checks: >- | ||||
|   *, | ||||
|   -abseil-*, | ||||
|   -altera-*, | ||||
|   -android-*, | ||||
|   -boost-*, | ||||
|   -bugprone-branch-clone, | ||||
|   -bugprone-easily-swappable-parameters, | ||||
|   -bugprone-narrowing-conversions, | ||||
|   -bugprone-signed-char-misuse, | ||||
|   -bugprone-too-small-loop-variable, | ||||
| @@ -14,7 +16,13 @@ Checks: >- | ||||
|   -cert-str34-c, | ||||
|   -clang-analyzer-optin.cplusplus.UninitializedObject, | ||||
|   -clang-analyzer-osx.*, | ||||
|   -clang-diagnostic-delete-abstract-non-virtual-dtor, | ||||
|   -clang-diagnostic-delete-non-abstract-non-virtual-dtor, | ||||
|   -clang-diagnostic-shadow-field, | ||||
|   -clang-diagnostic-sign-compare, | ||||
|   -clang-diagnostic-unused-variable, | ||||
|   -clang-diagnostic-unused-const-variable, | ||||
|   -concurrency-*, | ||||
|   -cppcoreguidelines-avoid-c-arrays, | ||||
|   -cppcoreguidelines-avoid-goto, | ||||
|   -cppcoreguidelines-avoid-magic-numbers, | ||||
| @@ -22,7 +30,6 @@ Checks: >- | ||||
|   -cppcoreguidelines-macro-usage, | ||||
|   -cppcoreguidelines-narrowing-conversions, | ||||
|   -cppcoreguidelines-non-private-member-variables-in-classes, | ||||
|   -cppcoreguidelines-owning-memory, | ||||
|   -cppcoreguidelines-pro-bounds-array-to-pointer-decay, | ||||
|   -cppcoreguidelines-pro-bounds-constant-array-index, | ||||
|   -cppcoreguidelines-pro-bounds-pointer-arithmetic, | ||||
| @@ -56,17 +63,21 @@ Checks: >- | ||||
|   -misc-no-recursion, | ||||
|   -misc-unused-parameters, | ||||
|   -modernize-avoid-c-arrays, | ||||
|   -modernize-avoid-bind, | ||||
|   -modernize-concat-nested-namespaces, | ||||
|   -modernize-return-braced-init-list, | ||||
|   -modernize-use-auto, | ||||
|   -modernize-use-default-member-init, | ||||
|   -modernize-use-equals-default, | ||||
|   -modernize-use-trailing-return-type, | ||||
|   -modernize-use-nodiscard, | ||||
|   -mpi-*, | ||||
|   -objc-*, | ||||
|   -readability-braces-around-statements, | ||||
|   -readability-const-return-type, | ||||
|   -readability-convert-member-functions-to-static, | ||||
|   -readability-else-after-return, | ||||
|   -readability-function-cognitive-complexity, | ||||
|   -readability-implicit-bool-conversion, | ||||
|   -readability-isolate-declaration, | ||||
|   -readability-magic-numbers, | ||||
| @@ -78,9 +89,7 @@ Checks: >- | ||||
|   -readability-redundant-string-init, | ||||
|   -readability-uppercase-literal-suffix, | ||||
|   -readability-use-anyofallof, | ||||
|   -warnings-as-errors | ||||
| WarningsAsErrors: '*' | ||||
| HeaderFilterRegex: '^.*/src/esphome/.*' | ||||
| AnalyzeTemporaryDtors: false | ||||
| FormatStyle:     google | ||||
| CheckOptions: | ||||
| @@ -104,6 +113,10 @@ CheckOptions: | ||||
|     value:           llvm | ||||
|   - key:             modernize-use-nullptr.NullMacros | ||||
|     value:           'NULL' | ||||
|   - key:             modernize-make-unique.MakeSmartPtrFunction | ||||
|     value:           'make_unique' | ||||
|   - key:             modernize-make-unique.MakeSmartPtrFunctionHeader | ||||
|     value:           'esphome/core/helpers.h' | ||||
|   - key:             readability-identifier-naming.LocalVariableCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.ClassCase | ||||
| @@ -117,15 +130,19 @@ CheckOptions: | ||||
|   - key:             readability-identifier-naming.StaticConstantCase | ||||
|     value:           'UPPER_CASE' | ||||
|   - key:             readability-identifier-naming.StaticVariableCase | ||||
|     value:           'UPPER_CASE' | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.GlobalConstantCase | ||||
|     value:           'UPPER_CASE' | ||||
|   - key:             readability-identifier-naming.ParameterCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.PrivateMemberPrefix | ||||
|     value:           'NO_PRIVATE_MEMBERS_ALWAYS_USE_PROTECTED' | ||||
|   - key:             readability-identifier-naming.PrivateMethodPrefix | ||||
|     value:           'NO_PRIVATE_METHODS_ALWAYS_USE_PROTECTED' | ||||
|   - key:             readability-identifier-naming.PrivateMemberCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.PrivateMemberSuffix | ||||
|     value:           '_' | ||||
|   - key:             readability-identifier-naming.PrivateMethodCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.PrivateMethodSuffix | ||||
|     value:           '_' | ||||
|   - key:             readability-identifier-naming.ClassMemberCase | ||||
|     value:           'lower_case' | ||||
|   - key:             readability-identifier-naming.ClassMemberCase | ||||
|   | ||||
| @@ -1,17 +1,29 @@ | ||||
| { | ||||
|   "name": "ESPHome Dev", | ||||
|   "context": "..", | ||||
|   "dockerFile": "../docker/Dockerfile.dev", | ||||
|   "postCreateCommand": "mkdir -p config && pip3 install -e .", | ||||
|   "runArgs": ["--privileged", "-e", "ESPHOME_DASHBOARD_USE_PING=1"], | ||||
|   "image": "esphome/esphome-lint:dev", | ||||
|   "postCreateCommand": [ | ||||
|     "script/devcontainer-post-create" | ||||
|   ], | ||||
|   "runArgs": [ | ||||
|     "--privileged", | ||||
|     "-e", | ||||
|     "ESPHOME_DASHBOARD_USE_PING=1" | ||||
|   ], | ||||
|   "appPort": 6052, | ||||
|   "extensions": [ | ||||
|     // python | ||||
|     "ms-python.python", | ||||
|     "visualstudioexptteam.vscodeintellicode", | ||||
|     "redhat.vscode-yaml" | ||||
|     // yaml | ||||
|     "redhat.vscode-yaml", | ||||
|     // cpp | ||||
|     "ms-vscode.cpptools", | ||||
|     // editorconfig | ||||
|     "editorconfig.editorconfig", | ||||
|   ], | ||||
|   "settings": { | ||||
|     "python.pythonPath": "/usr/local/bin/python", | ||||
|     "python.languageServer": "Pylance", | ||||
|     "python.pythonPath": "/usr/bin/python3", | ||||
|     "python.linting.pylintEnabled": true, | ||||
|     "python.linting.enabled": true, | ||||
|     "python.formatting.provider": "black", | ||||
| @@ -19,7 +31,7 @@ | ||||
|     "editor.formatOnSave": true, | ||||
|     "editor.formatOnType": true, | ||||
|     "files.trimTrailingWhitespace": true, | ||||
|     "terminal.integrated.shell.linux": "/bin/bash", | ||||
|     "terminal.integrated.defaultProfile.linux": "bash", | ||||
|     "yaml.customTags": [ | ||||
|       "!secret scalar", | ||||
|       "!lambda scalar", | ||||
| @@ -27,6 +39,18 @@ | ||||
|       "!include_dir_list scalar", | ||||
|       "!include_dir_merge_list scalar", | ||||
|       "!include_dir_merge_named scalar" | ||||
|     ] | ||||
|     ], | ||||
|     "files.exclude": { | ||||
|       "**/.git": true, | ||||
|       "**/.DS_Store": true, | ||||
|       "**/*.pyc": { | ||||
|         "when": "$(basename).py" | ||||
|       }, | ||||
|       "**/__pycache__": true | ||||
|     }, | ||||
|     "files.associations": { | ||||
|       "**/.vscode/*.json": "jsonc" | ||||
|     }, | ||||
|     "C_Cpp.clang_format_path": "/usr/bin/clang-format-11", | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -103,6 +103,10 @@ venv.bak/ | ||||
| # mypy | ||||
| .mypy_cache/ | ||||
|  | ||||
| # PlatformIO | ||||
| .pio/ | ||||
|  | ||||
| # ESPHome | ||||
| config/ | ||||
| examples/ | ||||
| Dockerfile | ||||
|   | ||||
| @@ -7,7 +7,7 @@ insert_final_newline = true | ||||
| charset = utf-8 | ||||
|  | ||||
| # python | ||||
| [*.{py}] | ||||
| [*.py] | ||||
| indent_style = space | ||||
| indent_size = 4 | ||||
|  | ||||
| @@ -25,4 +25,10 @@ indent_size = 2 | ||||
| [*.{yaml,yml}] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| quote_type = single | ||||
| quote_type = single | ||||
|  | ||||
| # JSON | ||||
| [*.json] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,2 @@ | ||||
| # Normalize line endings to LF in the repository | ||||
| * text eol=lf | ||||
							
								
								
									
										59
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										59
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,59 +0,0 @@ | ||||
| # Configuration for probot-stale - https://github.com/probot/stale | ||||
|  | ||||
| # Number of days of inactivity before an Issue or Pull Request becomes stale | ||||
| daysUntilStale: 60 | ||||
|  | ||||
| # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. | ||||
| # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. | ||||
| daysUntilClose: 7 | ||||
|  | ||||
| # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) | ||||
| onlyLabels: [] | ||||
|  | ||||
| # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable | ||||
| exemptLabels: | ||||
|   - not-stale | ||||
|  | ||||
| # Set to true to ignore issues in a project (defaults to false) | ||||
| exemptProjects: false | ||||
|  | ||||
| # Set to true to ignore issues in a milestone (defaults to false) | ||||
| exemptMilestones: true | ||||
|  | ||||
| # Set to true to ignore issues with an assignee (defaults to false) | ||||
| exemptAssignees: false | ||||
|  | ||||
| # Label to use when marking as stale | ||||
| staleLabel: stale | ||||
|  | ||||
| # Comment to post when marking as stale. Set to `false` to disable | ||||
| markComment: > | ||||
|   This issue has been automatically marked as stale because it has not had | ||||
|   recent activity. It will be closed if no further activity occurs. Thank you | ||||
|   for your contributions. | ||||
|  | ||||
| # Comment to post when removing the stale label. | ||||
| # unmarkComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Comment to post when closing a stale Issue or Pull Request. | ||||
| # closeComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Limit the number of actions per hour, from 1-30. Default is 30 | ||||
| limitPerRun: 10 | ||||
|  | ||||
| # Limit to only `issues` or `pulls` | ||||
| only: pulls | ||||
|  | ||||
| # Optionally, specify configuration settings that are specific to just 'issues' or 'pulls': | ||||
| # pulls: | ||||
| #   daysUntilStale: 30 | ||||
| #   markComment: > | ||||
| #     This pull request has been automatically marked as stale because it has not had | ||||
| #     recent activity. It will be closed if no further activity occurs. Thank you | ||||
| #     for your contributions. | ||||
|  | ||||
| # issues: | ||||
| #   exemptLabels: | ||||
| #     - confirmed | ||||
							
								
								
									
										56
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -3,53 +3,47 @@ name: CI for docker images | ||||
| # Only run when docker paths change | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev, beta, master] | ||||
|     branches: [dev, beta, release] | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|  | ||||
| jobs: | ||||
|   check-docker: | ||||
|     name: Build docker containers | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["hassio", "docker"] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up env variables | ||||
|         run: | | ||||
|           base_version="3.4.0" | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|  | ||||
|           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then | ||||
|             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-hassio-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile.hassio" | ||||
|           else | ||||
|             build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile" | ||||
|           fi | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|         echo "TAG=check" >> $GITHUB_ENV | ||||
|  | ||||
|           echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV | ||||
|           echo "BUILD_TO=${build_to}" >> $GITHUB_ENV | ||||
|           echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV | ||||
|       - name: Pull for cache | ||||
|         run: | | ||||
|           docker pull "${BUILD_TO}:dev" || true | ||||
|       - name: Register QEMU binfmt | ||||
|         run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes | ||||
|       - run: | | ||||
|           docker build \ | ||||
|             --build-arg "BUILD_FROM=${BUILD_FROM}" \ | ||||
|             --build-arg "BUILD_VERSION=ci" \ | ||||
|             --cache-from "${BUILD_TO}:dev" \ | ||||
|             --file "${DOCKERFILE}" \ | ||||
|             . | ||||
|     - name: Run build | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${TAG}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build | ||||
|   | ||||
							
								
								
									
										239
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										239
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -4,158 +4,153 @@ name: CI | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     # On dev branch release-dev already performs CI checks | ||||
|     # On other branches the `pull_request` trigger will be used | ||||
|     branches: [beta, master] | ||||
|     branches: [dev, beta, release] | ||||
|  | ||||
|   pull_request: | ||||
|  | ||||
| jobs: | ||||
|   lint-clang-format: | ||||
|   ci: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|  | ||||
|       - name: Run clang-format | ||||
|         run: script/clang-format -i | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-clang-tidy: | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         split: [1, 2, 3, 4] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|         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: test1 | ||||
|           - id: test | ||||
|             file: tests/test4.yaml | ||||
|             name: Test tests/test4.yaml | ||||
|             pio_cache_key: test4 | ||||
|           - id: test | ||||
|             file: tests/test5.yaml | ||||
|             name: Test tests/test5.yaml | ||||
|             pio_cache_key: test5 | ||||
|           - id: pytest | ||||
|             name: Run pytest | ||||
|           - id: clang-format | ||||
|             name: Run script/clang-format | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP8266 | ||||
|             options: --environment esp8266-tidy --grep USE_ESP8266 | ||||
|             pio_cache_key: tidyesp8266 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 1/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 1 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 2/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 2 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 3/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 3 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 4/4 | ||||
|             options: --environment esp32-tidy --split-num 4 --split-at 4 | ||||
|             pio_cache_key: tidyesp32 | ||||
|           - id: clang-tidy | ||||
|             name: Run script/clang-tidy for ESP32 esp-idf | ||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||
|             pio_cache_key: tidyesp32-idf | ||||
|  | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|       - name: Run clang-tidy | ||||
|         run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-python: | ||||
|     # Don't use the esphome-lint docker image because it may contain outdated requirements. | ||||
|     # This way, all dependencies are cached via the cache action. | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         id: python | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|  | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           key: pip-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|             pip-${{ steps.python.outputs.python-version }}- | ||||
|  | ||||
|       - name: Set up python environment | ||||
|         run: script/setup | ||||
|         run: | | ||||
|           pip3 install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||
|           pip3 install -e . | ||||
|  | ||||
|       # Use per check platformio cache because checks use different parts | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache@v2 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||
|         if: matrix.id == 'test' || matrix.id == 'clang-tidy' | ||||
|  | ||||
|       - name: Install clang tools | ||||
|         run: | | ||||
|           sudo apt-get install \ | ||||
|               clang-format-11 \ | ||||
|               clang-tidy-11 | ||||
|         if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/lint-python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/pytest.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|  | ||||
|       - name: Lint Custom | ||||
|         run: script/ci-custom.py | ||||
|         run: | | ||||
|           script/ci-custom.py | ||||
|           script/build_codeowners.py --check | ||||
|         if: matrix.id == 'ci-custom' | ||||
|  | ||||
|       - name: Lint Python | ||||
|         run: script/lint-python | ||||
|       - name: Lint CODEOWNERS | ||||
|         run: script/build_codeowners.py --check | ||||
|         if: matrix.id == 'lint-python' | ||||
|  | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|           test: | ||||
|           - test1 | ||||
|           - test2 | ||||
|           - test3 | ||||
|           - test4 | ||||
|           - test5 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       # Use per test platformio cache because tests have different platform versions | ||||
|       - name: Cache ~/.platformio | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} | ||||
|           restore-keys: | | ||||
|             test-home-platformio-${{ matrix.test }}- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|       - 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: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - run: esphome compile tests/${{ matrix.test }}.yaml | ||||
|  | ||||
|   pytest: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|       - name: Install Github Actions annotator | ||||
|         run: pip install pytest-github-actions-annotate-failures | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - name: Run pytest | ||||
|         run: | | ||||
|           pytest \ | ||||
|             -qq \ | ||||
|             --durations=10 \ | ||||
|             -o console_output_style=count \ | ||||
|             tests | ||||
|           pytest -vv --tb=native tests | ||||
|         if: matrix.id == 'pytest' | ||||
|  | ||||
|       # Also run git-diff-index so that the step is marked as failed on formatting errors, | ||||
|       # since clang-format doesn't do anything but change files if -i is passed. | ||||
|       - name: Run clang-format | ||||
|         run: | | ||||
|           script/clang-format -i | ||||
|           git diff-index --quiet HEAD -- | ||||
|         if: matrix.id == 'clang-format' | ||||
|  | ||||
|       - name: Run clang-tidy | ||||
|         run: | | ||||
|           script/clang-tidy --all-headers --fix ${{ matrix.options }} | ||||
|         if: matrix.id == 'clang-tidy' | ||||
|         env: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format') | ||||
|   | ||||
							
								
								
									
										42
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								.github/workflows/docker-lint-build.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,42 +0,0 @@ | ||||
| name: Build and publish lint docker image | ||||
|  | ||||
| # Only run when docker paths change | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev] | ||||
|     paths: | ||||
|       - 'docker/Dockerfile.lint' | ||||
|       - 'requirements.txt' | ||||
|       - 'requirements_optional.txt' | ||||
|       - 'requirements_test.txt' | ||||
|       - 'platformio.ini' | ||||
|       - '.github/workflows/docker-lint-build.yml' | ||||
|  | ||||
| jobs: | ||||
|   publish-docker-lint-iage: | ||||
|     name: Build docker containers | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|           echo "TAG=1.1" >> $GITHUB_ENV | ||||
|       - name: Pull for cache | ||||
|         run: | | ||||
|           docker pull "esphome/esphome-lint:latest" || true | ||||
|       - name: Build | ||||
|         run: | | ||||
|           docker build \ | ||||
|             --cache-from "esphome/esphome-lint:latest" \ | ||||
|             --file "docker/Dockerfile.lint" \ | ||||
|             --tag "esphome/esphome-lint:latest" \ | ||||
|             --tag "esphome/esphome-lint:${TAG}" \ | ||||
|             . | ||||
|       - name: Log in to docker hub | ||||
|         env: | ||||
|           DOCKER_USER: ${{ secrets.DOCKER_USER }} | ||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
|         run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" | ||||
|       - run: | | ||||
|           docker push "esphome/esphome-lint:${TAG}" | ||||
|           docker push "esphome/esphome-lint:latest" | ||||
							
								
								
									
										21
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| name: Lock | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@v2 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           pr-lock-inactive-days: "1" | ||||
|           pr-lock-reason: "" | ||||
|           process-only: prs | ||||
							
								
								
									
										19
									
								
								.github/workflows/matchers/pytest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								.github/workflows/matchers/pytest.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| { | ||||
|   "problemMatcher": [ | ||||
|     { | ||||
|       "owner": "pytest", | ||||
|       "fileLocation": "absolute", | ||||
|       "pattern": [ | ||||
|         { | ||||
|           "regexp": "^\\s+File \"(.*)\", line (\\d+), in (.*)$", | ||||
|           "file": 1, | ||||
|           "line": 2 | ||||
|         }, | ||||
|         { | ||||
|           "regexp": "^\\s+(.*)$", | ||||
|           "message": 1 | ||||
|         } | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										247
									
								
								.github/workflows/release-dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										247
									
								
								.github/workflows/release-dev.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,247 +0,0 @@ | ||||
| name: Publish dev releases to docker hub | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: | ||||
|     - dev | ||||
|  | ||||
| jobs: | ||||
|   # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml | ||||
|  | ||||
|   lint-clang-format: | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|  | ||||
|       - name: Run clang-format | ||||
|         run: script/clang-format -i | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-clang-tidy: | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         split: [1, 2, 3, 4] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|  | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|       - name: Run clang-tidy | ||||
|         run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-python: | ||||
|     # Don't use the esphome-lint docker image because it may contain outdated requirements. | ||||
|     # This way, all dependencies are cached via the cache action. | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       - name: Set up python environment | ||||
|         run: script/setup | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/lint-python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - name: Lint Custom | ||||
|         run: script/ci-custom.py | ||||
|       - name: Lint Python | ||||
|         run: script/lint-python | ||||
|       - name: Lint CODEOWNERS | ||||
|         run: script/build_codeowners.py --check | ||||
|  | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|           test: | ||||
|           - test1 | ||||
|           - test2 | ||||
|           - test3 | ||||
|           - test4 | ||||
|           - test5 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       # Use per test platformio cache because tests have different platform versions | ||||
|       - name: Cache ~/.platformio | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} | ||||
|           restore-keys: | | ||||
|             test-home-platformio-${{ matrix.test }}- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|  | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - run: esphome compile tests/${{ matrix.test }}.yaml | ||||
|  | ||||
|   pytest: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|       - name: Install Github Actions annotator | ||||
|         run: pip install pytest-github-actions-annotate-failures | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - name: Run pytest | ||||
|         run: | | ||||
|           pytest \ | ||||
|             -qq \ | ||||
|             --durations=10 \ | ||||
|             -o console_output_style=count \ | ||||
|             tests | ||||
|  | ||||
|   deploy-docker: | ||||
|     name: Build and publish docker containers | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] | ||||
|     strategy: | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         # Hassio dev image doesn't use esphome/esphome-hassio-$arch and uses base directly | ||||
|         build_type: ["docker"] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|           TAG="${GITHUB_SHA:0:7}" | ||||
|           echo "TAG=${TAG}" >> $GITHUB_ENV | ||||
|       - name: Set up env variables | ||||
|         run: | | ||||
|           base_version="3.4.0" | ||||
|  | ||||
|           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then | ||||
|             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-hassio-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile.hassio" | ||||
|           else | ||||
|             build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile" | ||||
|           fi | ||||
|  | ||||
|           echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV | ||||
|           echo "BUILD_TO=${build_to}" >> $GITHUB_ENV | ||||
|           echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV | ||||
|       - name: Pull for cache | ||||
|         run: | | ||||
|           docker pull "${BUILD_TO}:dev" || true | ||||
|       - name: Register QEMU binfmt | ||||
|         run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes | ||||
|       - run: | | ||||
|           docker build \ | ||||
|             --build-arg "BUILD_FROM=${BUILD_FROM}" \ | ||||
|             --build-arg "BUILD_VERSION=${TAG}" \ | ||||
|             --tag "${BUILD_TO}:${TAG}" \ | ||||
|             --tag "${BUILD_TO}:dev" \ | ||||
|             --cache-from "${BUILD_TO}:dev" \ | ||||
|             --file "${DOCKERFILE}" \ | ||||
|             . | ||||
|       - name: Log in to docker hub | ||||
|         env: | ||||
|           DOCKER_USER: ${{ secrets.DOCKER_USER }} | ||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
|         run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" | ||||
|       - run: | | ||||
|           docker push "${BUILD_TO}:${TAG}" | ||||
|           docker push "${BUILD_TO}:dev" | ||||
|  | ||||
|  | ||||
|   deploy-docker-manifest: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [deploy-docker] | ||||
|     steps: | ||||
|     - name: Enable experimental manifest support | ||||
|       run: | | ||||
|         mkdir -p ~/.docker | ||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|         TAG="${GITHUB_SHA:0:7}" | ||||
|         echo "TAG=${TAG}" >> $GITHUB_ENV | ||||
|     - name: Log in to docker hub | ||||
|       env: | ||||
|         DOCKER_USER: ${{ secrets.DOCKER_USER }} | ||||
|         DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" | ||||
|     - name: "Create the manifest" | ||||
|       run: | | ||||
|         docker manifest create esphome/esphome:${TAG} \ | ||||
|           esphome/esphome-aarch64:${TAG} \ | ||||
|           esphome/esphome-amd64:${TAG} \ | ||||
|           esphome/esphome-armv7:${TAG} | ||||
|         docker manifest push esphome/esphome:${TAG} | ||||
|  | ||||
|         docker manifest create esphome/esphome:dev \ | ||||
|           esphome/esphome-aarch64:${TAG} \ | ||||
|           esphome/esphome-amd64:${TAG} \ | ||||
|           esphome/esphome-armv7:${TAG} | ||||
|         docker manifest push esphome/esphome:dev | ||||
							
								
								
									
										317
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										317
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,164 +1,35 @@ | ||||
| name: Publish Release | ||||
|  | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   release: | ||||
|     types: [published] | ||||
|   schedule: | ||||
|     - cron: "0 2 * * *" | ||||
|  | ||||
| jobs: | ||||
|   # THE LINT/TEST JOBS ARE COPIED FROM ci.yaml | ||||
|  | ||||
|   lint-clang-format: | ||||
|   init: | ||||
|     name: Initialize build | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.tag }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|  | ||||
|       - name: Run clang-format | ||||
|         run: script/clang-format -i | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-clang-tidy: | ||||
|     runs-on: ubuntu-latest | ||||
|     # cpp lint job runs with esphome-lint docker image so that clang-format-* | ||||
|     # doesn't have to be installed | ||||
|     container: esphome/esphome-lint:1.1 | ||||
|     # Split clang-tidy check into 4 jobs. Each one will check 1/4th of the .cpp files | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|         split: [1, 2, 3, 4] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       # Set up the pio project so that the cpp checks know how files are compiled | ||||
|       # (build flags, libraries etc) | ||||
|       - name: Set up platformio environment | ||||
|         run: pio init --ide atom | ||||
|  | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|       - name: Run clang-tidy | ||||
|         run: script/clang-tidy --all-headers --fix --split-num 4 --split-at ${{ matrix.split }} | ||||
|       - name: Suggest changes | ||||
|         run: script/ci-suggest-changes | ||||
|  | ||||
|   lint-python: | ||||
|     # Don't use the esphome-lint docker image because it may contain outdated requirements. | ||||
|     # This way, all dependencies are cached via the cache action. | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       - name: Set up python environment | ||||
|         run: script/setup | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/lint-python.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - name: Lint Custom | ||||
|         run: script/ci-custom.py | ||||
|       - name: Lint Python | ||||
|         run: script/lint-python | ||||
|       - name: Lint CODEOWNERS | ||||
|         run: script/build_codeowners.py --check | ||||
|  | ||||
|   test: | ||||
|     runs-on: ubuntu-latest | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       matrix: | ||||
|           test: | ||||
|           - test1 | ||||
|           - test2 | ||||
|           - test3 | ||||
|           - test4 | ||||
|           - test5 | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       # Use per test platformio cache because tests have different platform versions | ||||
|       - name: Cache ~/.platformio | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: test-home-platformio-${{ matrix.test }}-${{ hashFiles('esphome/core/config.py') }} | ||||
|           restore-keys: | | ||||
|             test-home-platformio-${{ matrix.test }}- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - run: esphome compile tests/${{ matrix.test }}.yaml | ||||
|  | ||||
|   pytest: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: '3.7' | ||||
|       - name: Cache pip modules | ||||
|         uses: actions/cache@v1 | ||||
|         with: | ||||
|           path: ~/.cache/pip | ||||
|           key: esphome-pip-3.7-${{ hashFiles('setup.py') }} | ||||
|           restore-keys: | | ||||
|             esphome-pip-3.7- | ||||
|       - name: Set up environment | ||||
|         run: script/setup | ||||
|       - name: Install Github Actions annotator | ||||
|         run: pip install pytest-github-actions-annotate-failures | ||||
|  | ||||
|       - name: Register problem matchers | ||||
|         run: | | ||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" | ||||
|       - name: Run pytest | ||||
|         run: | | ||||
|           pytest \ | ||||
|             -qq \ | ||||
|             --durations=10 \ | ||||
|             -o console_output_style=count \ | ||||
|             tests | ||||
|           if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then | ||||
|             TAG="${GITHUB_REF#refs/tags/}" | ||||
|           else | ||||
|             TAG=$(cat esphome/const.py | sed -n -E "s/^__version__\s+=\s+\"(.+)\"$/\1/p") | ||||
|             today="$(date --utc '+%Y%m%d')" | ||||
|             TAG="${TAG}${today}" | ||||
|           fi | ||||
|           echo "::set-output name=tag::${TAG}" | ||||
|  | ||||
|   deploy-pypi: | ||||
|     name: Build and publish to PyPi | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
| @@ -182,129 +53,93 @@ jobs: | ||||
|     name: Build and publish docker containers | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [lint-clang-format, lint-clang-tidy, lint-python, test, pytest] | ||||
|     needs: [init] | ||||
|     strategy: | ||||
|       matrix: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["hassio", "docker"] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|           TAG="${GITHUB_REF#refs/tags/v}" | ||||
|           echo "TAG=${TAG}" >> $GITHUB_ENV | ||||
|       - name: Set up env variables | ||||
|         run: | | ||||
|           base_version="3.4.0" | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|  | ||||
|           if [[ "${{ matrix.build_type }}" == "hassio" ]]; then | ||||
|             build_from="esphome/esphome-hassio-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-hassio-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile.hassio" | ||||
|           else | ||||
|             build_from="esphome/esphome-base-${{ matrix.arch }}:${base_version}" | ||||
|             build_to="esphome/esphome-${{ matrix.arch }}" | ||||
|             dockerfile="docker/Dockerfile" | ||||
|           fi | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|  | ||||
|           if [[ "${{ github.event.release.prerelease }}" == "true" ]]; then | ||||
|             cache_tag="beta" | ||||
|           else | ||||
|             cache_tag="latest" | ||||
|           fi | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|         username: ${{ secrets.DOCKER_USER }} | ||||
|         password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|     - name: Log in to the GitHub container registry | ||||
|       uses: docker/login-action@v1 | ||||
|       with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|           # Set env variables so these values don't need to be calculated again | ||||
|           echo "BUILD_FROM=${build_from}" >> $GITHUB_ENV | ||||
|           echo "BUILD_TO=${build_to}" >> $GITHUB_ENV | ||||
|           echo "DOCKERFILE=${dockerfile}" >> $GITHUB_ENV | ||||
|           echo "CACHE_TAG=${cache_tag}" >> $GITHUB_ENV | ||||
|       - name: Pull for cache | ||||
|         run: | | ||||
|           docker pull "${BUILD_TO}:${CACHE_TAG}" || true | ||||
|       - name: Register QEMU binfmt | ||||
|         run: docker run --rm --privileged multiarch/qemu-user-static:5.2.0-2 --reset -p yes | ||||
|       - run: | | ||||
|           docker build \ | ||||
|             --build-arg "BUILD_FROM=${BUILD_FROM}" \ | ||||
|             --build-arg "BUILD_VERSION=${TAG}" \ | ||||
|             --tag "${BUILD_TO}:${TAG}" \ | ||||
|             --cache-from "${BUILD_TO}:${CACHE_TAG}" \ | ||||
|             --file "${DOCKERFILE}" \ | ||||
|             . | ||||
|       - name: Log in to docker hub | ||||
|         env: | ||||
|           DOCKER_USER: ${{ secrets.DOCKER_USER }} | ||||
|           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
|         run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" | ||||
|       - run: docker push "${BUILD_TO}:${TAG}" | ||||
|  | ||||
|       # Always publish to beta tag (also full releases) | ||||
|       - name: Publish docker beta tag | ||||
|         run: | | ||||
|           docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:beta" | ||||
|           docker push "${BUILD_TO}:beta" | ||||
|  | ||||
|       - if: ${{ !github.event.release.prerelease }} | ||||
|         name: Publish docker latest tag | ||||
|         run: | | ||||
|           docker tag "${BUILD_TO}:${TAG}" "${BUILD_TO}:latest" | ||||
|           docker push "${BUILD_TO}:latest" | ||||
|     - name: Build and push | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build \ | ||||
|           --push | ||||
|  | ||||
|   deploy-docker-manifest: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [deploy-docker] | ||||
|     needs: [init, deploy-docker] | ||||
|     strategy: | ||||
|       matrix: | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Enable experimental manifest support | ||||
|       run: | | ||||
|         mkdir -p ~/.docker | ||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|         TAG="${GITHUB_REF#refs/tags/v}" | ||||
|         echo "TAG=${TAG}" >> $GITHUB_ENV | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       env: | ||||
|         DOCKER_USER: ${{ secrets.DOCKER_USER }} | ||||
|         DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       run: docker login -u "${DOCKER_USER}" -p "${DOCKER_PASSWORD}" | ||||
|     - name: "Create the manifest" | ||||
|       run: | | ||||
|         docker manifest create esphome/esphome:${TAG} \ | ||||
|           esphome/esphome-aarch64:${TAG} \ | ||||
|           esphome/esphome-amd64:${TAG} \ | ||||
|           esphome/esphome-armv7:${TAG} | ||||
|         docker manifest push esphome/esphome:${TAG} | ||||
|       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: Publish docker beta tag | ||||
|     - name: Run manifest | ||||
|       run: | | ||||
|         docker manifest create esphome/esphome:beta \ | ||||
|           esphome/esphome-aarch64:${TAG} \ | ||||
|           esphome/esphome-amd64:${TAG} \ | ||||
|           esphome/esphome-armv7:${TAG} | ||||
|         docker manifest push esphome/esphome:beta | ||||
|  | ||||
|     - name: Publish docker latest tag | ||||
|       if: ${{ !github.event.release.prerelease }} | ||||
|       run: | | ||||
|         docker manifest create esphome/esphome:latest \ | ||||
|           esphome/esphome-aarch64:${TAG} \ | ||||
|           esphome/esphome-amd64:${TAG} \ | ||||
|           esphome/esphome-armv7:${TAG} | ||||
|         docker manifest push esphome/esphome:latest | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           manifest | ||||
|  | ||||
|   deploy-hassio-repo: | ||||
|     if: github.repository == 'esphome/esphome' | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     needs: [deploy-docker] | ||||
|     steps: | ||||
|       - env: | ||||
|           TOKEN: ${{ secrets.DEPLOY_HASSIO_TOKEN }} | ||||
|         run: | | ||||
|           TAG="${GITHUB_REF#refs/tags/v}" | ||||
|           TAG="${GITHUB_REF#refs/tags/}" | ||||
|           curl \ | ||||
|             -u ":$TOKEN" \ | ||||
|             -X POST \ | ||||
|             -H "Accept: application/vnd.github.v3+json" \ | ||||
|             https://api.github.com/repos/esphome/hassio/actions/workflows/bump-version.yml/dispatches \ | ||||
|             -d "{\"ref\":\"master\",\"inputs\":{\"version\":\"$TAG\"}}" | ||||
|             -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" | ||||
|   | ||||
							
								
								
									
										30
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| name: Stale | ||||
|  | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
|   issues: write | ||||
|   pull-requests: write | ||||
|  | ||||
| jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|         with: | ||||
|           repo-token: ${{ github.token }} | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
|           days-before-issue-stale: -1 | ||||
|           days-before-issue-close: -1 | ||||
|           remove-stale-when-updated: true | ||||
|           stale-pr-label: "stale" | ||||
|           exempt-pr-labels: "no-stale" | ||||
|           stale-pr-message: > | ||||
|             There hasn't been any activity on this pull request recently. This | ||||
|             pull request has been automatically marked as stale because of that | ||||
|             and will be closed if no further activity occurs within 7 days. | ||||
|             Thank you for your contributions. | ||||
							
								
								
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -13,6 +13,9 @@ __pycache__/ | ||||
| # Intellij Idea | ||||
| .idea | ||||
|  | ||||
| # Vim | ||||
| *.swp | ||||
|  | ||||
| # Hide some OS X stuff | ||||
| .DS_Store | ||||
| .AppleDouble | ||||
| @@ -99,10 +102,7 @@ CMakeLists.txt | ||||
| .idea/**/dynamic.xml | ||||
|  | ||||
| # CMake | ||||
| cmake-build-debug/ | ||||
| cmake-build-livingroom8266/ | ||||
| cmake-build-livingroom32/ | ||||
| cmake-build-release/ | ||||
| cmake-build-*/ | ||||
|  | ||||
| CMakeCache.txt | ||||
| CMakeFiles | ||||
| @@ -122,4 +122,8 @@ config/ | ||||
| tests/build/ | ||||
| tests/.esphome/ | ||||
| /.temp-clang-tidy.cpp | ||||
| /.temp/ | ||||
| .pio/ | ||||
|  | ||||
| sdkconfig.* | ||||
| !sdkconfig.defaults | ||||
|   | ||||
| @@ -23,5 +23,5 @@ repos: | ||||
|       - id: no-commit-to-branch | ||||
|         args: | ||||
|           - --branch=dev | ||||
|           - --branch=master | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|   | ||||
							
								
								
									
										35
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										35
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,11 +1,32 @@ | ||||
| { | ||||
|     "version": "2.0.0", | ||||
|     "tasks": [ | ||||
|   "version": "2.0.0", | ||||
|   "tasks": [ | ||||
|     { | ||||
|       "label": "run", | ||||
|       "type": "shell", | ||||
|       "command": "python3 -m esphome dashboard config/", | ||||
|       "problemMatcher": [] | ||||
|     }, | ||||
|     { | ||||
|       "label": "clang-tidy", | ||||
|       "type": "shell", | ||||
|       "command": "./script/clang-tidy", | ||||
|       "problemMatcher": [ | ||||
|         { | ||||
|             "label": "run", | ||||
|             "type": "shell", | ||||
|             "command": "python3 -m esphome dashboard config/", | ||||
|             "problemMatcher": [] | ||||
|           "owner": "clang-tidy", | ||||
|           "fileLocation": "absolute", | ||||
|           "pattern": [ | ||||
|             { | ||||
|               "regexp": "^(.*):(\\d+):(\\d+):\\s+(error):\\s+(.*) \\[([a-z0-9,\\-]+)\\]\\s*$", | ||||
|               "file": 1, | ||||
|               "line": 2, | ||||
|               "column": 3, | ||||
|               "severity": 4, | ||||
|               "message": 5 | ||||
|             } | ||||
|           ] | ||||
|         } | ||||
|     ] | ||||
|       ] | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
							
								
								
									
										58
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -14,31 +14,46 @@ esphome/core/* @esphome/core | ||||
| esphome/components/ac_dimmer/* @glmnet | ||||
| esphome/components/adc/* @esphome/core | ||||
| esphome/components/addressable_light/* @justfalter | ||||
| esphome/components/airthings_ble/* @jeromelaban | ||||
| esphome/components/airthings_wave_mini/* @ncareau | ||||
| esphome/components/airthings_wave_plus/* @jeromelaban | ||||
| esphome/components/am43/* @buxtronix | ||||
| esphome/components/am43/cover/* @buxtronix | ||||
| esphome/components/animation/* @syndlex | ||||
| esphome/components/anova/* @buxtronix | ||||
| esphome/components/api/* @OttoWinter | ||||
| esphome/components/async_tcp/* @OttoWinter | ||||
| esphome/components/atc_mithermometer/* @ahpohl | ||||
| esphome/components/b_parasite/* @rbaron | ||||
| esphome/components/ballu/* @bazuchan | ||||
| esphome/components/bang_bang/* @OttoWinter | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/ble_client/* @buxtronix | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/canbus/* @danielschramm @mvturnho | ||||
| esphome/components/captive_portal/* @OttoWinter | ||||
| esphome/components/ccs811/* @habbie | ||||
| esphome/components/climate/* @esphome/core | ||||
| esphome/components/climate_ir/* @glmnet | ||||
| esphome/components/color_temperature/* @jesserockz | ||||
| esphome/components/coolix/* @glmnet | ||||
| esphome/components/cover/* @esphome/core | ||||
| esphome/components/cs5460a/* @balrog-kun | ||||
| esphome/components/ct_clamp/* @jesserockz | ||||
| esphome/components/current_based/* @djwmarcx | ||||
| esphome/components/daly_bms/* @s1lvi0 | ||||
| esphome/components/dashboard_import/* @esphome/core | ||||
| esphome/components/debug/* @OttoWinter | ||||
| esphome/components/dfplayer/* @glmnet | ||||
| esphome/components/dht/* @OttoWinter | ||||
| esphome/components/ds1307/* @badbadc0ffee | ||||
| esphome/components/dsmr/* @glmnet @zuidwijk | ||||
| esphome/components/esp32/* @esphome/core | ||||
| esphome/components/esp32_ble/* @jesserockz | ||||
| esphome/components/esp32_ble_controller/* @jesserockz | ||||
| esphome/components/esp32_ble_server/* @jesserockz | ||||
| esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| esphome/components/ezo/* @ssieb | ||||
| esphome/components/fastled_base/* @OttoWinter | ||||
| @@ -46,7 +61,14 @@ esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||
| esphome/components/globals/* @esphome/core | ||||
| esphome/components/gpio/* @esphome/core | ||||
| esphome/components/gps/* @coogle | ||||
| esphome/components/graph/* @synco | ||||
| esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
| esphome/components/homeassistant/* @OttoWinter | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/improv/* @jesserockz | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| @@ -57,6 +79,7 @@ esphome/components/json/* @OttoWinter | ||||
| esphome/components/ledc/* @OttoWinter | ||||
| esphome/components/light/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/mcp23008/* @jesserockz | ||||
| esphome/components/mcp23017/* @jesserockz | ||||
| @@ -67,33 +90,58 @@ esphome/components/mcp23x17_base/* @jesserockz | ||||
| esphome/components/mcp23xxx_base/* @jesserockz | ||||
| esphome/components/mcp2515/* @danielschramm @mvturnho | ||||
| esphome/components/mcp9808/* @k7hpn | ||||
| esphome/components/midea_ac/* @dudanov | ||||
| esphome/components/midea_dongle/* @dudanov | ||||
| esphome/components/mdns/* @esphome/core | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| esphome/components/modbus_controller/* @martgras | ||||
| esphome/components/modbus_controller/binary_sensor/* @martgras | ||||
| esphome/components/modbus_controller/number/* @martgras | ||||
| esphome/components/modbus_controller/output/* @martgras | ||||
| esphome/components/modbus_controller/sensor/* @martgras | ||||
| esphome/components/modbus_controller/switch/* @martgras | ||||
| esphome/components/modbus_controller/text_sensor/* @martgras | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @senexcrenshaw | ||||
| esphome/components/nextion/binary_sensor/* @senexcrenshaw | ||||
| esphome/components/nextion/sensor/* @senexcrenshaw | ||||
| esphome/components/nextion/switch/* @senexcrenshaw | ||||
| esphome/components/nextion/text_sensor/* @senexcrenshaw | ||||
| esphome/components/nfc/* @jesserockz | ||||
| esphome/components/number/* @esphome/core | ||||
| esphome/components/ota/* @esphome/core | ||||
| esphome/components/output/* @esphome/core | ||||
| esphome/components/pid/* @OttoWinter | ||||
| esphome/components/pipsolar/* @andreashergert1984 | ||||
| esphome/components/pm1006/* @habbie | ||||
| esphome/components/pmsa003i/* @sjtrny | ||||
| esphome/components/pn532/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532_i2c/* @OttoWinter @jesserockz | ||||
| esphome/components/pn532_spi/* @OttoWinter @jesserockz | ||||
| esphome/components/power_supply/* @esphome/core | ||||
| esphome/components/preferences/* @esphome/core | ||||
| esphome/components/pulse_meter/* @stevebaxter | ||||
| esphome/components/pvvx_mithermometer/* @pasiz | ||||
| esphome/components/rc522/* @glmnet | ||||
| esphome/components/rc522_i2c/* @glmnet | ||||
| esphome/components/rc522_spi/* @glmnet | ||||
| esphome/components/restart/* @esphome/core | ||||
| esphome/components/rf_bridge/* @jesserockz | ||||
| esphome/components/rgbct/* @jesserockz | ||||
| esphome/components/rtttl/* @glmnet | ||||
| esphome/components/safe_mode/* @paulmonigatti | ||||
| esphome/components/scd4x/* @sjtrny | ||||
| esphome/components/script/* @esphome/core | ||||
| esphome/components/sdm_meter/* @jesserockz @polyfaces | ||||
| esphome/components/sdp3x/* @Azimath | ||||
| esphome/components/selec_meter/* @sourabhjaiswal | ||||
| esphome/components/select/* @esphome/core | ||||
| esphome/components/sensor/* @esphome/core | ||||
| esphome/components/sgp40/* @SenexCrenshaw | ||||
| esphome/components/sht4x/* @sjtrny | ||||
| esphome/components/shutdown/* @esphome/core | ||||
| esphome/components/sim800l/* @glmnet | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/spi/* @esphome/core | ||||
| esphome/components/ssd1322_base/* @kbx81 | ||||
| esphome/components/ssd1322_spi/* @kbx81 | ||||
| @@ -108,17 +156,23 @@ esphome/components/ssd1351_base/* @kbx81 | ||||
| esphome/components/ssd1351_spi/* @kbx81 | ||||
| esphome/components/st7735/* @SenexCrenshaw | ||||
| esphome/components/st7789v/* @kbx81 | ||||
| esphome/components/st7920/* @marsjan155 | ||||
| esphome/components/substitutions/* @esphome/core | ||||
| esphome/components/sun/* @OttoWinter | ||||
| esphome/components/switch/* @esphome/core | ||||
| esphome/components/t6615/* @tylermenezes | ||||
| esphome/components/tca9548a/* @andreashergert1984 | ||||
| esphome/components/tcl112/* @glmnet | ||||
| esphome/components/teleinfo/* @0hax | ||||
| esphome/components/thermostat/* @kbx81 | ||||
| esphome/components/time/* @OttoWinter | ||||
| esphome/components/tlc5947/* @rnauber | ||||
| esphome/components/tm1637/* @glmnet | ||||
| esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| esphome/components/toshiba/* @kbx81 | ||||
| esphome/components/tsl2591/* @wjcarpenter | ||||
| esphome/components/tuya/binary_sensor/* @jesserockz | ||||
| esphome/components/tuya/climate/* @jesserockz | ||||
| esphome/components/tuya/sensor/* @jesserockz | ||||
|   | ||||
| @@ -1,10 +1,6 @@ | ||||
| # Contributing to ESPHome | ||||
|  | ||||
| This python project is responsible for reading in YAML configuration files, | ||||
| converting them to C++ code. This code is then converted to a platformio project and compiled | ||||
| with [esphome-core](https://github.com/esphome/esphome-core), the C++ framework behind the project. | ||||
|  | ||||
| For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphomeyaml | ||||
| For a detailed guide, please see https://esphome.io/guides/contributing.html#contributing-to-esphome | ||||
|  | ||||
| Things to note when contributing: | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| # ESPHome [](https://travis-ci.org/esphome/esphome) [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) | ||||
| # ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) | ||||
|  | ||||
| [](https://esphome.io/) | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,60 @@ | ||||
| ARG BUILD_FROM=esphome/esphome-base-amd64:3.4.0 | ||||
| FROM ${BUILD_FROM} | ||||
| # Build these with the build.py script | ||||
| # Example: | ||||
| #   python3 docker/build.py --tag dev --arch amd64 --build-type docker build | ||||
|  | ||||
| # One of "docker", "hassio" | ||||
| ARG BASEIMGTYPE=docker | ||||
|  | ||||
| FROM ghcr.io/hassio-addons/debian-base/amd64:5.1.0 AS base-hassio-amd64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/aarch64:5.1.0 AS base-hassio-arm64 | ||||
| FROM ghcr.io/hassio-addons/debian-base/armv7:5.1.0 AS base-hassio-armv7 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-amd64 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-arm64 | ||||
| FROM debian:bullseye-20210902-slim AS base-docker-armv7 | ||||
|  | ||||
| # Use TARGETARCH/TARGETVARIANT defined by docker | ||||
| # https://docs.docker.com/engine/reference/builder/#automatic-platform-args-in-the-global-scope | ||||
| FROM base-${BASEIMGTYPE}-${TARGETARCH}${TARGETVARIANT} AS base | ||||
|  | ||||
| RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         python3=3.9.2-3 \ | ||||
|         python3-pip=20.3.4-4 \ | ||||
|         python3-setuptools=52.0.0-4 \ | ||||
|         python3-pil=8.1.2+dfsg-0.3 \ | ||||
|         python3-cryptography=3.3.2-1 \ | ||||
|         iputils-ping=3:20210202-1 \ | ||||
|         git=1:2.30.2-1 \ | ||||
|         curl=7.74.0-1.3+b1 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| ENV \ | ||||
|   # Fix click python3 lang warning https://click.palletsprojects.com/en/7.x/python3/ | ||||
|   LANG=C.UTF-8 LC_ALL=C.UTF-8 \ | ||||
|   # Store globally installed pio libs in /piolibs | ||||
|   PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
|  | ||||
| RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     pip3 install --no-cache-dir \ | ||||
|         wheel==0.36.2 \ | ||||
|         platformio==5.2.0 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_libraries_interval 1000000 \ | ||||
|     && platformio settings set check_platformio_interval 1000000 \ | ||||
|     && platformio settings set check_platforms_interval 1000000 \ | ||||
|     && mkdir -p /piolibs | ||||
|  | ||||
|  | ||||
|  | ||||
| # ======================= docker-type image ======================= | ||||
| FROM base AS docker | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| @@ -7,9 +62,9 @@ RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|  | ||||
| # Then copy esphome and install | ||||
| COPY . . | ||||
| RUN pip3 install --no-cache-dir -e . | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN pip3 install --no-cache-dir -e /esphome | ||||
|  | ||||
| # Settings for dashboard | ||||
| ENV USERNAME="" PASSWORD="" | ||||
| @@ -17,14 +72,85 @@ ENV USERNAME="" PASSWORD="" | ||||
| # Expose the dashboard to Docker | ||||
| EXPOSE 6052 | ||||
|  | ||||
| # Run healthcheck (heartbeat) | ||||
| HEALTHCHECK --interval=30s --timeout=30s \ | ||||
|   CMD curl --fail http://localhost:6052 || exit 1 | ||||
| COPY docker/docker_entrypoint.sh /entrypoint.sh | ||||
|  | ||||
| # The directory the user should mount their configuration files to | ||||
| VOLUME /config | ||||
| WORKDIR /config | ||||
| # Set entrypoint to esphome so that the user doesn't have to type 'esphome' | ||||
| # Set entrypoint to esphome (via a script) so that the user doesn't have to type 'esphome' | ||||
| # in every docker command twice | ||||
| ENTRYPOINT ["esphome"] | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ||||
| # When no arguments given, start the dashboard in the workdir | ||||
| CMD ["dashboard", "/config"] | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # ======================= hassio-type image ======================= | ||||
| FROM base AS hassio | ||||
|  | ||||
| RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         nginx=1.18.0-6.1 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| ARG BUILD_VERSION=dev | ||||
|  | ||||
| # Copy root filesystem | ||||
| COPY docker/hassio-rootfs/ / | ||||
|  | ||||
| # First install requirements to leverage caching when requirements don't change | ||||
| COPY requirements.txt requirements_optional.txt docker/platformio_install_deps.py platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|  | ||||
| # Copy esphome and install | ||||
| COPY . /esphome | ||||
| RUN pip3 install --no-cache-dir -e /esphome | ||||
|  | ||||
| # Labels | ||||
| LABEL \ | ||||
|     io.hass.name="ESPHome" \ | ||||
|     io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \ | ||||
|     io.hass.type="addon" \ | ||||
|     io.hass.version="${BUILD_VERSION}" | ||||
|     # io.hass.arch is inherited from addon-debian-base | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| # ======================= lint-type image ======================= | ||||
| FROM base AS lint | ||||
|  | ||||
| ENV \ | ||||
|   PLATFORMIO_CORE_DIR=/esphome/.temp/platformio | ||||
|  | ||||
| RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         clang-format-11=1:11.0.1-2 \ | ||||
|         clang-tidy-11=1:11.0.1-2 \ | ||||
|         patch=2.7.6-7 \ | ||||
|         software-properties-common=0.96.20.2-2.1 \ | ||||
|         nano=5.4-2 \ | ||||
|         build-essential=12.9 \ | ||||
|         python3-dev=3.9.2-3 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
|         /var/lib/apt/lists/* | ||||
|  | ||||
| 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 | ||||
|  | ||||
| VOLUME ["/esphome"] | ||||
| WORKDIR /esphome | ||||
|   | ||||
| @@ -1,13 +0,0 @@ | ||||
| FROM esphome/esphome-base-amd64:3.4.0 | ||||
|  | ||||
| COPY . . | ||||
|  | ||||
| RUN apt-get update \ | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         python3-wheel \ | ||||
|         net-tools \ | ||||
|     && apt-get clean \ | ||||
|     && rm -rf /var/lib/apt/lists/* | ||||
|  | ||||
| WORKDIR /workspaces | ||||
| ENV SHELL /bin/bash | ||||
| @@ -1,25 +0,0 @@ | ||||
| ARG BUILD_FROM | ||||
| FROM ${BUILD_FROM} | ||||
|  | ||||
| # 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 root filesystem | ||||
| COPY docker/rootfs/ / | ||||
|  | ||||
| # Then copy esphome and install | ||||
| COPY . /opt/esphome/ | ||||
| RUN pip3 install --no-cache-dir -e /opt/esphome | ||||
|  | ||||
| # Build arguments | ||||
| ARG BUILD_VERSION=dev | ||||
|  | ||||
| # Labels | ||||
| LABEL \ | ||||
|     io.hass.name="ESPHome" \ | ||||
|     io.hass.description="Manage and program ESP8266/ESP32 microcontrollers through YAML configuration files" \ | ||||
|     io.hass.type="addon" \ | ||||
|     io.hass.version=${BUILD_VERSION} | ||||
| @@ -1,9 +0,0 @@ | ||||
| FROM esphome/esphome-lint-base:3.4.0 | ||||
|  | ||||
| COPY requirements.txt requirements_optional.txt requirements_test.txt docker/platformio_install_deps.py  platformio.ini / | ||||
| RUN \ | ||||
|     pip3 install --no-cache-dir -r /requirements.txt -r /requirements_optional.txt -r /requirements_test.txt \ | ||||
|     && /platformio_install_deps.py /platformio.ini | ||||
|  | ||||
| VOLUME ["/esphome"] | ||||
| WORKDIR /esphome | ||||
							
								
								
									
										159
									
								
								docker/build.py
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										159
									
								
								docker/build.py
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,159 @@ | ||||
| #!/usr/bin/env python3 | ||||
| from dataclasses import dataclass | ||||
| import subprocess | ||||
| import argparse | ||||
| from platform import machine | ||||
| import shlex | ||||
| import re | ||||
| import sys | ||||
|  | ||||
|  | ||||
| CHANNEL_DEV = 'dev' | ||||
| CHANNEL_BETA = 'beta' | ||||
| CHANNEL_RELEASE = 'release' | ||||
| CHANNELS = [CHANNEL_DEV, CHANNEL_BETA, CHANNEL_RELEASE] | ||||
|  | ||||
| ARCH_AMD64 = 'amd64' | ||||
| ARCH_ARMV7 = 'armv7' | ||||
| ARCH_AARCH64 = 'aarch64' | ||||
| ARCHS = [ARCH_AMD64, ARCH_ARMV7, ARCH_AARCH64] | ||||
|  | ||||
| TYPE_DOCKER = 'docker' | ||||
| TYPE_HA_ADDON = 'ha-addon' | ||||
| TYPE_LINT = 'lint' | ||||
| TYPES = [TYPE_DOCKER, TYPE_HA_ADDON, TYPE_LINT] | ||||
|  | ||||
|  | ||||
| parser = argparse.ArgumentParser() | ||||
| parser.add_argument("--tag", type=str, required=True, help="The main docker tag to push to. If a version number also adds latest and/or beta tag") | ||||
| parser.add_argument("--arch", choices=ARCHS, required=False, help="The architecture to build for") | ||||
| parser.add_argument("--build-type", choices=TYPES, required=True, help="The type of build to run") | ||||
| parser.add_argument("--dry-run", action="store_true", help="Don't run any commands, just print them") | ||||
| subparsers = parser.add_subparsers(help="Action to perform", dest="command", required=True) | ||||
| build_parser = subparsers.add_parser("build", help="Build the image") | ||||
| build_parser.add_argument("--push", help="Also push the images", action="store_true") | ||||
| manifest_parser = subparsers.add_parser("manifest", help="Create a manifest from already pushed images") | ||||
|  | ||||
|  | ||||
| @dataclass(frozen=True) | ||||
| class DockerParams: | ||||
|     build_to: str | ||||
|     manifest_to: str | ||||
|     baseimgtype: str | ||||
|     platform: str | ||||
|     target: str | ||||
|  | ||||
|     @classmethod | ||||
|     def for_type_arch(cls, build_type, arch): | ||||
|         prefix = { | ||||
|             TYPE_DOCKER: "esphome/esphome", | ||||
|             TYPE_HA_ADDON: "esphome/esphome-hassio", | ||||
|             TYPE_LINT: "esphome/esphome-lint" | ||||
|         }[build_type] | ||||
|         build_to = f"{prefix}-{arch}" | ||||
|         baseimgtype = { | ||||
|             TYPE_DOCKER: "docker", | ||||
|             TYPE_HA_ADDON: "hassio", | ||||
|             TYPE_LINT: "docker", | ||||
|         }[build_type] | ||||
|         platform = { | ||||
|             ARCH_AMD64: "linux/amd64", | ||||
|             ARCH_ARMV7: "linux/arm/v7", | ||||
|             ARCH_AARCH64: "linux/arm64", | ||||
|         }[arch] | ||||
|         target = { | ||||
|             TYPE_DOCKER: "docker", | ||||
|             TYPE_HA_ADDON: "hassio", | ||||
|             TYPE_LINT: "lint", | ||||
|         }[build_type] | ||||
|         return cls( | ||||
|             build_to=build_to, | ||||
|             manifest_to=prefix, | ||||
|             baseimgtype=baseimgtype, | ||||
|             platform=platform, | ||||
|             target=target, | ||||
|         ) | ||||
|  | ||||
|  | ||||
| def main(): | ||||
|     args = parser.parse_args() | ||||
|  | ||||
|     def run_command(*cmd, ignore_error: bool = False): | ||||
|         print(f"$ {shlex.join(list(cmd))}") | ||||
|         if not args.dry_run: | ||||
|             rc = subprocess.call(list(cmd)) | ||||
|             if rc != 0 and not ignore_error: | ||||
|                 print("Command failed") | ||||
|                 sys.exit(1) | ||||
|  | ||||
|     # detect channel from tag | ||||
|     match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag) | ||||
|     if match is None: | ||||
|         channel = CHANNEL_DEV | ||||
|     elif match.group(1) is None: | ||||
|         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 args.command == "build": | ||||
|         # 1. pull cache image | ||||
|         params = DockerParams.for_type_arch(args.build_type, args.arch) | ||||
|         cache_tag = { | ||||
|             CHANNEL_DEV: "cache-dev", | ||||
|             CHANNEL_BETA: "cache-beta", | ||||
|             CHANNEL_RELEASE: "cache-latest", | ||||
|         }[channel] | ||||
|         cache_img = f"ghcr.io/{params.build_to}:{cache_tag}" | ||||
|  | ||||
|         imgs = [f"{params.build_to}:{tag}" for tag in tags_to_push] | ||||
|         imgs += [f"ghcr.io/{params.build_to}:{tag}" for tag in tags_to_push] | ||||
|  | ||||
|         # 3. build | ||||
|         cmd = [ | ||||
|             "docker", "buildx", "build", | ||||
|             "--build-arg", f"BASEIMGTYPE={params.baseimgtype}", | ||||
|             "--build-arg", f"BUILD_VERSION={args.tag}", | ||||
|             "--cache-from", f"type=registry,ref={cache_img}", | ||||
|             "--file", "docker/Dockerfile", | ||||
|             "--platform", params.platform, | ||||
|             "--target", params.target, | ||||
|         ] | ||||
|         for img in imgs: | ||||
|             cmd += ["--tag", img] | ||||
|         if args.push: | ||||
|             cmd += ["--push", "--cache-to", f"type=registry,ref={cache_img},mode=max"] | ||||
|  | ||||
|         run_command(*cmd, ".") | ||||
|     elif args.command == "manifest": | ||||
|         manifest = DockerParams.for_type_arch(args.build_type, ARCH_AMD64).manifest_to | ||||
|  | ||||
|         targets = [f"{manifest}:{tag}" for tag in tags_to_push] | ||||
|         targets += [f"ghcr.io/{manifest}:{tag}" for tag in tags_to_push] | ||||
|         # 1. Create manifests | ||||
|         for target in targets: | ||||
|             cmd = ["docker", "manifest", "create", target] | ||||
|             for arch in ARCHS: | ||||
|                 src = f"{DockerParams.for_type_arch(args.build_type, arch).build_to}:{args.tag}" | ||||
|                 if target.startswith("ghcr.io"): | ||||
|                     src = f"ghcr.io/{src}" | ||||
|                 cmd.append(src) | ||||
|             run_command(*cmd) | ||||
|         # 2. Push manifests | ||||
|         for target in targets: | ||||
|             run_command( | ||||
|                 "docker", "manifest", "push", target | ||||
|             ) | ||||
|  | ||||
|  | ||||
| if __name__ == "__main__": | ||||
|     main() | ||||
							
								
								
									
										24
									
								
								docker/docker_entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										24
									
								
								docker/docker_entrypoint.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| #!/bin/bash | ||||
|  | ||||
| # If /cache is mounted, use that as PIO's coredir | ||||
| # otherwise use path in /config (so that PIO packages aren't downloaded on each compile) | ||||
|  | ||||
| if [[ -d /cache ]]; then | ||||
|     pio_cache_base=/cache/platformio | ||||
| else | ||||
|     pio_cache_base=/config/.esphome/platformio | ||||
| fi | ||||
|  | ||||
| if [[ ! -d "${pio_cache_base}" ]]; then | ||||
|     echo "Creating cache directory ${pio_cache_base}" | ||||
|     echo "You can change this behavior by mounting a directory to the container's /cache directory." | ||||
|     mkdir -p "${pio_cache_base}" | ||||
| fi | ||||
|  | ||||
| # 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" | ||||
|  | ||||
| exec esphome "$@" | ||||
							
								
								
									
										9
									
								
								docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								docker/hassio-rootfs/etc/cont-init.d/30-dirs.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | ||||
| #!/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}" | ||||
| @@ -22,5 +22,14 @@ if bashio::config.has_value 'relative_url'; then | ||||
|     export ESPHOME_DASHBOARD_RELATIVE_URL=$(bashio::config 'relative_url') | ||||
| fi | ||||
| 
 | ||||
| pio_cache_base=/data/cache/platformio | ||||
| # we can't set core_dir, because the settings file is stored in `core_dir/appstate.json` | ||||
| # setting `core_dir` would therefore prevent pio from accessing | ||||
| export PLATFORMIO_PLATFORMS_DIR="${pio_cache_base}/platforms" | ||||
| export PLATFORMIO_PACKAGES_DIR="${pio_cache_base}/packages" | ||||
| export PLATFORMIO_CACHE_DIR="${pio_cache_base}/cache" | ||||
| 
 | ||||
| export PLATFORMIO_GLOBALLIB_DIR=/piolibs | ||||
| 
 | ||||
| bashio::log.info "Starting ESPHome dashboard..." | ||||
| exec esphome dashboard /config/esphome --socket /var/run/esphome.sock --hassio | ||||
| @@ -3,18 +3,11 @@ | ||||
| # all platformio libraries in the global storage | ||||
|  | ||||
| import configparser | ||||
| import re | ||||
| import subprocess | ||||
| import sys | ||||
|  | ||||
| config = configparser.ConfigParser() | ||||
| config = configparser.ConfigParser(inline_comment_prefixes=(';', )) | ||||
| config.read(sys.argv[1]) | ||||
| libs = [] | ||||
| for line in config['common']['lib_deps'].splitlines(): | ||||
|     # Format: '1655@1.0.2  ; TinyGPSPlus (has name conflict)' (includes comment) | ||||
|     m = re.search(r'([a-zA-Z0-9-_/]+@[0-9\.]+)', line) | ||||
|     if m is None: | ||||
|         continue | ||||
|     libs.append(m.group(1)) | ||||
| libs = [x for x in config['common']['lib_deps'].splitlines() if len(x) != 0] | ||||
|  | ||||
| subprocess.check_call(['platformio', 'lib', '-g', 'install', *libs]) | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # This files installs the user ESPHome version if specified | ||||
| # ============================================================================== | ||||
|  | ||||
| declare esphome_version | ||||
|  | ||||
| if bashio::config.has_value 'esphome_version'; then | ||||
|     esphome_version=$(bashio::config 'esphome_version') | ||||
|     if [[ $esphome_version == *":"* ]]; then | ||||
|       IFS=':' read -r -a array <<< "$esphome_version" | ||||
|       username=${array[0]} | ||||
|       ref=${array[1]} | ||||
|     else | ||||
|       username="esphome" | ||||
|       ref=$esphome_version | ||||
|     fi | ||||
|     full_url="https://github.com/${username}/esphome/archive/${ref}.zip" | ||||
|     bashio::log.info "Installing esphome version '${esphome_version}' (${full_url})..." | ||||
|     pip3 install -U --no-cache-dir "${full_url}" \ | ||||
|       || bashio::exit.nok "Failed installing esphome pinned version." | ||||
| fi | ||||
| @@ -1,11 +0,0 @@ | ||||
| #!/usr/bin/with-contenv bashio | ||||
| # ============================================================================== | ||||
| # Community Hass.io Add-ons: ESPHome | ||||
| # This files migrates the esphome config directory from the old path | ||||
| # ============================================================================== | ||||
|  | ||||
| if [[ ! -d /config/esphome && -d /config/esphomeyaml ]]; then | ||||
|     echo "Moving config directory from /config/esphomeyaml to /config/esphome" | ||||
|     mv /config/esphomeyaml /config/esphome | ||||
|     mv /config/esphome/.esphomeyaml /config/esphome/.esphome | ||||
| fi | ||||
| @@ -11,6 +11,7 @@ from esphome.config import iter_components, read_config, strip_default_ids | ||||
| from esphome.const import ( | ||||
|     CONF_BAUD_RATE, | ||||
|     CONF_BROKER, | ||||
|     CONF_DEASSERT_RTS_DTR, | ||||
|     CONF_LOGGER, | ||||
|     CONF_OTA, | ||||
|     CONF_PASSWORD, | ||||
| @@ -71,7 +72,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | ||||
|         if default == "OTA": | ||||
|             return CORE.address | ||||
|     if show_mqtt and "mqtt" in CORE.config: | ||||
|         options.append(("MQTT ({})".format(CORE.config["mqtt"][CONF_BROKER]), "MQTT")) | ||||
|         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) | ||||
|         if default == "OTA": | ||||
|             return "MQTT" | ||||
|     if default is not None: | ||||
| @@ -99,10 +100,21 @@ def run_miniterm(config, port): | ||||
|     baud_rate = config["logger"][CONF_BAUD_RATE] | ||||
|     if baud_rate == 0: | ||||
|         _LOGGER.info("UART logging is disabled (baud_rate=0). Not starting UART logs.") | ||||
|         return | ||||
|     _LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate) | ||||
|  | ||||
|     backtrace_state = False | ||||
|     with serial.Serial(port, baudrate=baud_rate) as ser: | ||||
|     ser = serial.Serial() | ||||
|     ser.baudrate = baud_rate | ||||
|     ser.port = port | ||||
|  | ||||
|     # We can't set to False by default since it leads to toggling and hence | ||||
|     # ESP32 resets on some platforms. | ||||
|     if config["logger"][CONF_DEASSERT_RTS_DTR]: | ||||
|         ser.dtr = False | ||||
|         ser.rts = False | ||||
|  | ||||
|     with ser: | ||||
|         while True: | ||||
|             try: | ||||
|                 raw = ser.readline() | ||||
| @@ -172,12 +184,30 @@ def compile_program(args, config): | ||||
|  | ||||
|  | ||||
| def upload_using_esptool(config, port): | ||||
|     path = CORE.firmware_bin | ||||
|     from esphome import platformio_api | ||||
|  | ||||
|     first_baudrate = config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get( | ||||
|         "upload_speed", 460800 | ||||
|     ) | ||||
|  | ||||
|     def run_esptool(baud_rate): | ||||
|         idedata = platformio_api.get_idedata(config) | ||||
|  | ||||
|         firmware_offset = "0x10000" if CORE.is_esp32 else "0x0" | ||||
|         flash_images = [ | ||||
|             platformio_api.FlashImage( | ||||
|                 path=idedata.firmware_bin_path, | ||||
|                 offset=firmware_offset, | ||||
|             ), | ||||
|             *idedata.extra_flash_images, | ||||
|         ] | ||||
|  | ||||
|         mcu = "esp8266" | ||||
|         if CORE.is_esp32: | ||||
|             from esphome.components.esp32 import get_esp32_variant | ||||
|  | ||||
|             mcu = get_esp32_variant().lower() | ||||
|  | ||||
|         cmd = [ | ||||
|             "esptool.py", | ||||
|             "--before", | ||||
| @@ -186,14 +216,15 @@ def upload_using_esptool(config, port): | ||||
|             "hard_reset", | ||||
|             "--baud", | ||||
|             str(baud_rate), | ||||
|             "--chip", | ||||
|             "esp8266", | ||||
|             "--port", | ||||
|             port, | ||||
|             "--chip", | ||||
|             mcu, | ||||
|             "write_flash", | ||||
|             "0x0", | ||||
|             path, | ||||
|             "-z", | ||||
|         ] | ||||
|         for img in flash_images: | ||||
|             cmd += [img.offset, img.path] | ||||
|  | ||||
|         if os.environ.get("ESPHOME_USE_SUBPROCESS") is None: | ||||
|             import esptool | ||||
| @@ -217,11 +248,7 @@ def upload_using_esptool(config, port): | ||||
| def upload_program(config, args, host): | ||||
|     # if upload is to a serial port use platformio, otherwise assume ota | ||||
|     if get_port_type(host) == "SERIAL": | ||||
|         from esphome import platformio_api | ||||
|  | ||||
|         if CORE.is_esp8266: | ||||
|             return upload_using_esptool(config, host) | ||||
|         return platformio_api.run_upload(config, CORE.verbose, host) | ||||
|         return upload_using_esptool(config, host) | ||||
|  | ||||
|     from esphome import espota2 | ||||
|  | ||||
| @@ -233,7 +260,7 @@ def upload_program(config, args, host): | ||||
|  | ||||
|     ota_conf = config[CONF_OTA] | ||||
|     remote_port = ota_conf[CONF_PORT] | ||||
|     password = ota_conf[CONF_PASSWORD] | ||||
|     password = ota_conf.get(CONF_PASSWORD, "") | ||||
|     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) | ||||
|  | ||||
|  | ||||
| @@ -244,7 +271,7 @@ def show_logs(config, args, port): | ||||
|         run_miniterm(config, port) | ||||
|         return 0 | ||||
|     if get_port_type(port) == "NETWORK" and "api" in config: | ||||
|         from esphome.api.client import run_logs | ||||
|         from esphome.components.api.client import run_logs | ||||
|  | ||||
|         return run_logs(config, port) | ||||
|     if get_port_type(port) == "MQTT" and "mqtt" in config: | ||||
| @@ -284,7 +311,6 @@ def command_vscode(args): | ||||
|  | ||||
|     logging.disable(logging.INFO) | ||||
|     logging.disable(logging.WARNING) | ||||
|     CORE.config_path = args.configuration | ||||
|     vscode.read_config(args) | ||||
|  | ||||
|  | ||||
| @@ -404,30 +430,30 @@ def command_update_all(args): | ||||
|         click.echo(f"{half_line}{middle_text}{half_line}") | ||||
|  | ||||
|     for f in files: | ||||
|         print("Updating {}".format(color(Fore.CYAN, f))) | ||||
|         print(f"Updating {color(Fore.CYAN, f)}") | ||||
|         print("-" * twidth) | ||||
|         print() | ||||
|         rc = run_external_process( | ||||
|             "esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA" | ||||
|         ) | ||||
|         if rc == 0: | ||||
|             print_bar("[{}] {}".format(color(Fore.BOLD_GREEN, "SUCCESS"), f)) | ||||
|             print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}") | ||||
|             success[f] = True | ||||
|         else: | ||||
|             print_bar("[{}] {}".format(color(Fore.BOLD_RED, "ERROR"), f)) | ||||
|             print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}") | ||||
|             success[f] = False | ||||
|  | ||||
|         print() | ||||
|         print() | ||||
|         print() | ||||
|  | ||||
|     print_bar("[{}]".format(color(Fore.BOLD_WHITE, "SUMMARY"))) | ||||
|     print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]") | ||||
|     failed = 0 | ||||
|     for f in files: | ||||
|         if success[f]: | ||||
|             print("  - {}: {}".format(f, color(Fore.GREEN, "SUCCESS"))) | ||||
|             print(f"  - {f}: {color(Fore.GREEN, 'SUCCESS')}") | ||||
|         else: | ||||
|             print("  - {}: {}".format(f, color(Fore.BOLD_RED, "FAILED"))) | ||||
|             print(f"  - {f}: {color(Fore.BOLD_RED, 'FAILED')}") | ||||
|             failed += 1 | ||||
|     return failed | ||||
|  | ||||
| @@ -472,61 +498,9 @@ def parse_args(argv): | ||||
|         metavar=("key", "value"), | ||||
|     ) | ||||
|  | ||||
|     # Keep backward compatibility with the old command line format of | ||||
|     # esphome <config> <command>. | ||||
|     # | ||||
|     # Unfortunately this can't be done by adding another configuration argument to the | ||||
|     # main config parser, as argparse is greedy when parsing arguments, so in regular | ||||
|     # usage it'll eat the command as the configuration argument and error out out | ||||
|     # because it can't parse the configuration as a command. | ||||
|     # | ||||
|     # Instead, construct an ad-hoc parser for the old format that doesn't actually | ||||
|     # process the arguments, but parses them enough to let us figure out if the old | ||||
|     # format is used. In that case, swap the command and configuration in the arguments | ||||
|     # and continue on with the normal parser (after raising a deprecation warning). | ||||
|     # | ||||
|     # Disable argparse's built-in help option and add it manually to prevent this | ||||
|     # parser from printing the help messagefor the old format when invoked with -h. | ||||
|     compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) | ||||
|     compat_parser.add_argument("-h", "--help") | ||||
|     compat_parser.add_argument("configuration", nargs="*") | ||||
|     compat_parser.add_argument( | ||||
|         "command", | ||||
|         choices=[ | ||||
|             "config", | ||||
|             "compile", | ||||
|             "upload", | ||||
|             "logs", | ||||
|             "run", | ||||
|             "clean-mqtt", | ||||
|             "wizard", | ||||
|             "mqtt-fingerprint", | ||||
|             "version", | ||||
|             "clean", | ||||
|             "dashboard", | ||||
|         ], | ||||
|     ) | ||||
|  | ||||
|     # on Python 3.9+ we can simply set exit_on_error=False in the constructor | ||||
|     def _raise(x): | ||||
|         raise argparse.ArgumentError(None, x) | ||||
|  | ||||
|     compat_parser.error = _raise | ||||
|  | ||||
|     try: | ||||
|         result, unparsed = compat_parser.parse_known_args(argv[1:]) | ||||
|         last_option = len(argv) - len(unparsed) - 1 - len(result.configuration) | ||||
|         argv = argv[0:last_option] + [result.command] + result.configuration + unparsed | ||||
|         deprecated_argv_suggestion = argv | ||||
|     except argparse.ArgumentError: | ||||
|         # This is not an old-style command line, so we don't have to do anything. | ||||
|         deprecated_argv_suggestion = None | ||||
|  | ||||
|     # And continue on with regular parsing | ||||
|     parser = argparse.ArgumentParser( | ||||
|         description=f"ESPHome v{const.__version__}", parents=[options_parser] | ||||
|     ) | ||||
|     parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) | ||||
|  | ||||
|     mqtt_options = argparse.ArgumentParser(add_help=False) | ||||
|     mqtt_options.add_argument("--topic", help="Manually set the MQTT topic.") | ||||
| @@ -668,17 +642,91 @@ def parse_args(argv): | ||||
|     ) | ||||
|  | ||||
|     parser_vscode = subparsers.add_parser("vscode") | ||||
|     parser_vscode.add_argument( | ||||
|         "configuration", help="Your YAML configuration file.", nargs=1 | ||||
|     ) | ||||
|     parser_vscode.add_argument("configuration", help="Your YAML configuration file.") | ||||
|     parser_vscode.add_argument("--ace", action="store_true") | ||||
|  | ||||
|     parser_update = subparsers.add_parser("update-all") | ||||
|     parser_update.add_argument( | ||||
|         "configuration", help="Your YAML configuration file directory.", nargs=1 | ||||
|         "configuration", help="Your YAML configuration file directories.", nargs="+" | ||||
|     ) | ||||
|  | ||||
|     return parser.parse_args(argv[1:]) | ||||
|     # Keep backward compatibility with the old command line format of | ||||
|     # esphome <config> <command>. | ||||
|     # | ||||
|     # Unfortunately this can't be done by adding another configuration argument to the | ||||
|     # main config parser, as argparse is greedy when parsing arguments, so in regular | ||||
|     # usage it'll eat the command as the configuration argument and error out out | ||||
|     # because it can't parse the configuration as a command. | ||||
|     # | ||||
|     # Instead, if parsing using the current format fails, construct an ad-hoc parser | ||||
|     # that doesn't actually process the arguments, but parses them enough to let us | ||||
|     # figure out if the old format is used. In that case, swap the command and | ||||
|     # configuration in the arguments and retry with the normal parser (and raise | ||||
|     # a deprecation warning). | ||||
|     arguments = argv[1:] | ||||
|  | ||||
|     # On Python 3.9+ we can simply set exit_on_error=False in the constructor | ||||
|     def _raise(x): | ||||
|         raise argparse.ArgumentError(None, x) | ||||
|  | ||||
|     # First, try new-style parsing, but don't exit in case of failure | ||||
|     try: | ||||
|         # duplicate parser so that we can use the original one to raise errors later on | ||||
|         current_parser = argparse.ArgumentParser(add_help=False, parents=[parser]) | ||||
|         current_parser.set_defaults(deprecated_argv_suggestion=None) | ||||
|         current_parser.error = _raise | ||||
|         return current_parser.parse_args(arguments) | ||||
|     except argparse.ArgumentError: | ||||
|         pass | ||||
|  | ||||
|     # Second, try compat parsing and rearrange the command-line if it succeeds | ||||
|     # Disable argparse's built-in help option and add it manually to prevent this | ||||
|     # parser from printing the help messagefor the old format when invoked with -h. | ||||
|     compat_parser = argparse.ArgumentParser(parents=[options_parser], add_help=False) | ||||
|     compat_parser.add_argument("-h", "--help", action="store_true") | ||||
|     compat_parser.add_argument("configuration", nargs="*") | ||||
|     compat_parser.add_argument( | ||||
|         "command", | ||||
|         choices=[ | ||||
|             "config", | ||||
|             "compile", | ||||
|             "upload", | ||||
|             "logs", | ||||
|             "run", | ||||
|             "clean-mqtt", | ||||
|             "wizard", | ||||
|             "mqtt-fingerprint", | ||||
|             "version", | ||||
|             "clean", | ||||
|             "dashboard", | ||||
|             "vscode", | ||||
|             "update-all", | ||||
|         ], | ||||
|     ) | ||||
|  | ||||
|     try: | ||||
|         compat_parser.error = _raise | ||||
|         result, unparsed = compat_parser.parse_known_args(argv[1:]) | ||||
|         last_option = len(arguments) - len(unparsed) - 1 - len(result.configuration) | ||||
|         unparsed = [ | ||||
|             "--device" if arg in ("--upload-port", "--serial-port") else arg | ||||
|             for arg in unparsed | ||||
|         ] | ||||
|         arguments = ( | ||||
|             arguments[0:last_option] | ||||
|             + [result.command] | ||||
|             + result.configuration | ||||
|             + unparsed | ||||
|         ) | ||||
|         deprecated_argv_suggestion = arguments | ||||
|     except argparse.ArgumentError: | ||||
|         # old-style parsing failed, don't suggest any argument | ||||
|         deprecated_argv_suggestion = None | ||||
|  | ||||
|     # Finally, run the new-style parser again with the possibly swapped arguments, | ||||
|     # and let it error out if the command is unparsable. | ||||
|     parser.set_defaults(deprecated_argv_suggestion=deprecated_argv_suggestion) | ||||
|     return parser.parse_args(arguments) | ||||
|  | ||||
|  | ||||
| def run_esphome(argv): | ||||
| @@ -686,13 +734,13 @@ def run_esphome(argv): | ||||
|     CORE.dashboard = args.dashboard | ||||
|  | ||||
|     setup_log(args.verbose, args.quiet) | ||||
|     if args.deprecated_argv_suggestion is not None: | ||||
|     if args.deprecated_argv_suggestion is not None and args.command != "vscode": | ||||
|         _LOGGER.warning( | ||||
|             "Calling ESPHome with the configuration before the command is deprecated " | ||||
|             "and will be removed in the future. " | ||||
|         ) | ||||
|         _LOGGER.warning("Please instead use:") | ||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion[1:])) | ||||
|         _LOGGER.warning("   esphome %s", " ".join(args.deprecated_argv_suggestion)) | ||||
|  | ||||
|     if sys.version_info < (3, 7, 0): | ||||
|         _LOGGER.error( | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,518 +0,0 @@ | ||||
| from datetime import datetime | ||||
| import functools | ||||
| import logging | ||||
| import socket | ||||
| import threading | ||||
| import time | ||||
|  | ||||
| # pylint: disable=unused-import | ||||
| from typing import Optional  # noqa | ||||
| from google.protobuf import message  # noqa | ||||
|  | ||||
| from esphome import const | ||||
| import esphome.api.api_pb2 as pb | ||||
| from esphome.const import CONF_PASSWORD, CONF_PORT | ||||
| from esphome.core import EsphomeError | ||||
| from esphome.helpers import resolve_ip_address, indent | ||||
| from esphome.log import color, Fore | ||||
| from esphome.util import safe_print | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| class APIConnectionError(EsphomeError): | ||||
|     pass | ||||
|  | ||||
|  | ||||
| MESSAGE_TYPE_TO_PROTO = { | ||||
|     1: pb.HelloRequest, | ||||
|     2: pb.HelloResponse, | ||||
|     3: pb.ConnectRequest, | ||||
|     4: pb.ConnectResponse, | ||||
|     5: pb.DisconnectRequest, | ||||
|     6: pb.DisconnectResponse, | ||||
|     7: pb.PingRequest, | ||||
|     8: pb.PingResponse, | ||||
|     9: pb.DeviceInfoRequest, | ||||
|     10: pb.DeviceInfoResponse, | ||||
|     11: pb.ListEntitiesRequest, | ||||
|     12: pb.ListEntitiesBinarySensorResponse, | ||||
|     13: pb.ListEntitiesCoverResponse, | ||||
|     14: pb.ListEntitiesFanResponse, | ||||
|     15: pb.ListEntitiesLightResponse, | ||||
|     16: pb.ListEntitiesSensorResponse, | ||||
|     17: pb.ListEntitiesSwitchResponse, | ||||
|     18: pb.ListEntitiesTextSensorResponse, | ||||
|     19: pb.ListEntitiesDoneResponse, | ||||
|     20: pb.SubscribeStatesRequest, | ||||
|     21: pb.BinarySensorStateResponse, | ||||
|     22: pb.CoverStateResponse, | ||||
|     23: pb.FanStateResponse, | ||||
|     24: pb.LightStateResponse, | ||||
|     25: pb.SensorStateResponse, | ||||
|     26: pb.SwitchStateResponse, | ||||
|     27: pb.TextSensorStateResponse, | ||||
|     28: pb.SubscribeLogsRequest, | ||||
|     29: pb.SubscribeLogsResponse, | ||||
|     30: pb.CoverCommandRequest, | ||||
|     31: pb.FanCommandRequest, | ||||
|     32: pb.LightCommandRequest, | ||||
|     33: pb.SwitchCommandRequest, | ||||
|     34: pb.SubscribeServiceCallsRequest, | ||||
|     35: pb.ServiceCallResponse, | ||||
|     36: pb.GetTimeRequest, | ||||
|     37: pb.GetTimeResponse, | ||||
| } | ||||
|  | ||||
|  | ||||
| def _varuint_to_bytes(value): | ||||
|     if value <= 0x7F: | ||||
|         return bytes([value]) | ||||
|  | ||||
|     ret = bytes() | ||||
|     while value: | ||||
|         temp = value & 0x7F | ||||
|         value >>= 7 | ||||
|         if value: | ||||
|             ret += bytes([temp | 0x80]) | ||||
|         else: | ||||
|             ret += bytes([temp]) | ||||
|  | ||||
|     return ret | ||||
|  | ||||
|  | ||||
| def _bytes_to_varuint(value): | ||||
|     result = 0 | ||||
|     bitpos = 0 | ||||
|     for val in value: | ||||
|         result |= (val & 0x7F) << bitpos | ||||
|         bitpos += 7 | ||||
|         if (val & 0x80) == 0: | ||||
|             return result | ||||
|     return None | ||||
|  | ||||
|  | ||||
| # pylint: disable=too-many-instance-attributes,not-callable | ||||
| class APIClient(threading.Thread): | ||||
|     def __init__(self, address, port, password): | ||||
|         threading.Thread.__init__(self) | ||||
|         self._address = address  # type: str | ||||
|         self._port = port  # type: int | ||||
|         self._password = password  # type: Optional[str] | ||||
|         self._socket = None  # type: Optional[socket.socket] | ||||
|         self._socket_open_event = threading.Event() | ||||
|         self._socket_write_lock = threading.Lock() | ||||
|         self._connected = False | ||||
|         self._authenticated = False | ||||
|         self._message_handlers = [] | ||||
|         self._keepalive = 5 | ||||
|         self._ping_timer = None | ||||
|  | ||||
|         self.on_disconnect = None | ||||
|         self.on_connect = None | ||||
|         self.on_login = None | ||||
|         self.auto_reconnect = False | ||||
|         self._running_event = threading.Event() | ||||
|         self._stop_event = threading.Event() | ||||
|  | ||||
|     @property | ||||
|     def stopped(self): | ||||
|         return self._stop_event.is_set() | ||||
|  | ||||
|     def _refresh_ping(self): | ||||
|         if self._ping_timer is not None: | ||||
|             self._ping_timer.cancel() | ||||
|             self._ping_timer = None | ||||
|  | ||||
|         def func(): | ||||
|             self._ping_timer = None | ||||
|  | ||||
|             if self._connected: | ||||
|                 try: | ||||
|                     self.ping() | ||||
|                 except APIConnectionError as err: | ||||
|                     self._fatal_error(err) | ||||
|                 else: | ||||
|                     self._refresh_ping() | ||||
|  | ||||
|         self._ping_timer = threading.Timer(self._keepalive, func) | ||||
|         self._ping_timer.start() | ||||
|  | ||||
|     def _cancel_ping(self): | ||||
|         if self._ping_timer is not None: | ||||
|             self._ping_timer.cancel() | ||||
|             self._ping_timer = None | ||||
|  | ||||
|     def _close_socket(self): | ||||
|         self._cancel_ping() | ||||
|         if self._socket is not None: | ||||
|             self._socket.close() | ||||
|             self._socket = None | ||||
|         self._socket_open_event.clear() | ||||
|         self._connected = False | ||||
|         self._authenticated = False | ||||
|         self._message_handlers = [] | ||||
|  | ||||
|     def stop(self, force=False): | ||||
|         if self.stopped: | ||||
|             raise ValueError | ||||
|  | ||||
|         if self._connected and not force: | ||||
|             try: | ||||
|                 self.disconnect() | ||||
|             except APIConnectionError: | ||||
|                 pass | ||||
|         self._close_socket() | ||||
|  | ||||
|         self._stop_event.set() | ||||
|         if not force: | ||||
|             self.join() | ||||
|  | ||||
|     def connect(self): | ||||
|         if not self._running_event.wait(0.1): | ||||
|             raise APIConnectionError("You need to call start() first!") | ||||
|  | ||||
|         if self._connected: | ||||
|             self.disconnect(on_disconnect=False) | ||||
|  | ||||
|         try: | ||||
|             ip = resolve_ip_address(self._address) | ||||
|         except EsphomeError as err: | ||||
|             _LOGGER.warning( | ||||
|                 "Error resolving IP address of %s. Is it connected to WiFi?", | ||||
|                 self._address, | ||||
|             ) | ||||
|             _LOGGER.warning( | ||||
|                 "(If this error persists, please set a static IP address: " | ||||
|                 "https://esphome.io/components/wifi.html#manual-ips)" | ||||
|             ) | ||||
|             raise APIConnectionError(err) from err | ||||
|  | ||||
|         _LOGGER.info("Connecting to %s:%s (%s)", self._address, self._port, ip) | ||||
|         self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) | ||||
|         self._socket.settimeout(10.0) | ||||
|         self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) | ||||
|         try: | ||||
|             self._socket.connect((ip, self._port)) | ||||
|         except OSError as err: | ||||
|             err = APIConnectionError(f"Error connecting to {ip}: {err}") | ||||
|             self._fatal_error(err) | ||||
|             raise err | ||||
|         self._socket.settimeout(0.1) | ||||
|  | ||||
|         self._socket_open_event.set() | ||||
|  | ||||
|         hello = pb.HelloRequest() | ||||
|         hello.client_info = f"ESPHome v{const.__version__}" | ||||
|         try: | ||||
|             resp = self._send_message_await_response(hello, pb.HelloResponse) | ||||
|         except APIConnectionError as err: | ||||
|             self._fatal_error(err) | ||||
|             raise err | ||||
|         _LOGGER.debug( | ||||
|             "Successfully connected to %s ('%s' API=%s.%s)", | ||||
|             self._address, | ||||
|             resp.server_info, | ||||
|             resp.api_version_major, | ||||
|             resp.api_version_minor, | ||||
|         ) | ||||
|         self._connected = True | ||||
|         self._refresh_ping() | ||||
|         if self.on_connect is not None: | ||||
|             self.on_connect() | ||||
|  | ||||
|     def _check_connected(self): | ||||
|         if not self._connected: | ||||
|             err = APIConnectionError("Must be connected!") | ||||
|             self._fatal_error(err) | ||||
|             raise err | ||||
|  | ||||
|     def login(self): | ||||
|         self._check_connected() | ||||
|         if self._authenticated: | ||||
|             raise APIConnectionError("Already logged in!") | ||||
|  | ||||
|         connect = pb.ConnectRequest() | ||||
|         if self._password is not None: | ||||
|             connect.password = self._password | ||||
|         resp = self._send_message_await_response(connect, pb.ConnectResponse) | ||||
|         if resp.invalid_password: | ||||
|             raise APIConnectionError("Invalid password!") | ||||
|  | ||||
|         self._authenticated = True | ||||
|         if self.on_login is not None: | ||||
|             self.on_login() | ||||
|  | ||||
|     def _fatal_error(self, err): | ||||
|         was_connected = self._connected | ||||
|  | ||||
|         self._close_socket() | ||||
|  | ||||
|         if was_connected and self.on_disconnect is not None: | ||||
|             self.on_disconnect(err) | ||||
|  | ||||
|     def _write(self, data):  # type: (bytes) -> None | ||||
|         if self._socket is None: | ||||
|             raise APIConnectionError("Socket closed") | ||||
|  | ||||
|         # _LOGGER.debug("Write: %s", format_bytes(data)) | ||||
|         with self._socket_write_lock: | ||||
|             try: | ||||
|                 self._socket.sendall(data) | ||||
|             except OSError as err: | ||||
|                 err = APIConnectionError(f"Error while writing data: {err}") | ||||
|                 self._fatal_error(err) | ||||
|                 raise err | ||||
|  | ||||
|     def _send_message(self, msg): | ||||
|         # type: (message.Message) -> None | ||||
|         for message_type, klass in MESSAGE_TYPE_TO_PROTO.items(): | ||||
|             if isinstance(msg, klass): | ||||
|                 break | ||||
|         else: | ||||
|             raise ValueError | ||||
|  | ||||
|         encoded = msg.SerializeToString() | ||||
|         _LOGGER.debug("Sending %s:\n%s", type(msg), indent(str(msg))) | ||||
|         req = bytes([0]) | ||||
|         req += _varuint_to_bytes(len(encoded)) | ||||
|         req += _varuint_to_bytes(message_type) | ||||
|         req += encoded | ||||
|         self._write(req) | ||||
|  | ||||
|     def _send_message_await_response_complex( | ||||
|         self, send_msg, do_append, do_stop, timeout=5 | ||||
|     ): | ||||
|         event = threading.Event() | ||||
|         responses = [] | ||||
|  | ||||
|         def on_message(resp): | ||||
|             if do_append(resp): | ||||
|                 responses.append(resp) | ||||
|             if do_stop(resp): | ||||
|                 event.set() | ||||
|  | ||||
|         self._message_handlers.append(on_message) | ||||
|         self._send_message(send_msg) | ||||
|         ret = event.wait(timeout) | ||||
|         try: | ||||
|             self._message_handlers.remove(on_message) | ||||
|         except ValueError: | ||||
|             pass | ||||
|         if not ret: | ||||
|             raise APIConnectionError("Timeout while waiting for message response!") | ||||
|         return responses | ||||
|  | ||||
|     def _send_message_await_response(self, send_msg, response_type, timeout=5): | ||||
|         def is_response(msg): | ||||
|             return isinstance(msg, response_type) | ||||
|  | ||||
|         return self._send_message_await_response_complex( | ||||
|             send_msg, is_response, is_response, timeout | ||||
|         )[0] | ||||
|  | ||||
|     def device_info(self): | ||||
|         self._check_connected() | ||||
|         return self._send_message_await_response( | ||||
|             pb.DeviceInfoRequest(), pb.DeviceInfoResponse | ||||
|         ) | ||||
|  | ||||
|     def ping(self): | ||||
|         self._check_connected() | ||||
|         return self._send_message_await_response(pb.PingRequest(), pb.PingResponse) | ||||
|  | ||||
|     def disconnect(self, on_disconnect=True): | ||||
|         self._check_connected() | ||||
|  | ||||
|         try: | ||||
|             self._send_message_await_response( | ||||
|                 pb.DisconnectRequest(), pb.DisconnectResponse | ||||
|             ) | ||||
|         except APIConnectionError: | ||||
|             pass | ||||
|         self._close_socket() | ||||
|  | ||||
|         if self.on_disconnect is not None and on_disconnect: | ||||
|             self.on_disconnect(None) | ||||
|  | ||||
|     def _check_authenticated(self): | ||||
|         if not self._authenticated: | ||||
|             raise APIConnectionError("Must login first!") | ||||
|  | ||||
|     def subscribe_logs(self, on_log, log_level=7, dump_config=False): | ||||
|         self._check_authenticated() | ||||
|  | ||||
|         def on_msg(msg): | ||||
|             if isinstance(msg, pb.SubscribeLogsResponse): | ||||
|                 on_log(msg) | ||||
|  | ||||
|         self._message_handlers.append(on_msg) | ||||
|         req = pb.SubscribeLogsRequest(dump_config=dump_config) | ||||
|         req.level = log_level | ||||
|         self._send_message(req) | ||||
|  | ||||
|     def _recv(self, amount): | ||||
|         ret = bytes() | ||||
|         if amount == 0: | ||||
|             return ret | ||||
|  | ||||
|         while len(ret) < amount: | ||||
|             if self.stopped: | ||||
|                 raise APIConnectionError("Stopped!") | ||||
|             if not self._socket_open_event.is_set(): | ||||
|                 raise APIConnectionError("No socket!") | ||||
|             try: | ||||
|                 val = self._socket.recv(amount - len(ret)) | ||||
|             except AttributeError as err: | ||||
|                 raise APIConnectionError("Socket was closed") from err | ||||
|             except socket.timeout: | ||||
|                 continue | ||||
|             except OSError as err: | ||||
|                 raise APIConnectionError(f"Error while receiving data: {err}") from err | ||||
|             ret += val | ||||
|         return ret | ||||
|  | ||||
|     def _recv_varint(self): | ||||
|         raw = bytes() | ||||
|         while not raw or raw[-1] & 0x80: | ||||
|             raw += self._recv(1) | ||||
|         return _bytes_to_varuint(raw) | ||||
|  | ||||
|     def _run_once(self): | ||||
|         if not self._socket_open_event.wait(0.1): | ||||
|             return | ||||
|  | ||||
|         # Preamble | ||||
|         if self._recv(1)[0] != 0x00: | ||||
|             raise APIConnectionError("Invalid preamble") | ||||
|  | ||||
|         length = self._recv_varint() | ||||
|         msg_type = self._recv_varint() | ||||
|  | ||||
|         raw_msg = self._recv(length) | ||||
|         if msg_type not in MESSAGE_TYPE_TO_PROTO: | ||||
|             _LOGGER.debug("Skipping message type %s", msg_type) | ||||
|             return | ||||
|  | ||||
|         msg = MESSAGE_TYPE_TO_PROTO[msg_type]() | ||||
|         msg.ParseFromString(raw_msg) | ||||
|         _LOGGER.debug("Got message: %s:\n%s", type(msg), indent(str(msg))) | ||||
|         for msg_handler in self._message_handlers[:]: | ||||
|             msg_handler(msg) | ||||
|         self._handle_internal_messages(msg) | ||||
|  | ||||
|     def run(self): | ||||
|         self._running_event.set() | ||||
|         while not self.stopped: | ||||
|             try: | ||||
|                 self._run_once() | ||||
|             except APIConnectionError as err: | ||||
|                 if self.stopped: | ||||
|                     break | ||||
|                 if self._connected: | ||||
|                     _LOGGER.error("Error while reading incoming messages: %s", err) | ||||
|                     self._fatal_error(err) | ||||
|         self._running_event.clear() | ||||
|  | ||||
|     def _handle_internal_messages(self, msg): | ||||
|         if isinstance(msg, pb.DisconnectRequest): | ||||
|             self._send_message(pb.DisconnectResponse()) | ||||
|             if self._socket is not None: | ||||
|                 self._socket.close() | ||||
|                 self._socket = None | ||||
|             self._connected = False | ||||
|             if self.on_disconnect is not None: | ||||
|                 self.on_disconnect(None) | ||||
|         elif isinstance(msg, pb.PingRequest): | ||||
|             self._send_message(pb.PingResponse()) | ||||
|         elif isinstance(msg, pb.GetTimeRequest): | ||||
|             resp = pb.GetTimeResponse() | ||||
|             resp.epoch_seconds = int(time.time()) | ||||
|             self._send_message(resp) | ||||
|  | ||||
|  | ||||
| def run_logs(config, address): | ||||
|     conf = config["api"] | ||||
|     port = conf[CONF_PORT] | ||||
|     password = conf[CONF_PASSWORD] | ||||
|     _LOGGER.info("Starting log output from %s using esphome API", address) | ||||
|  | ||||
|     cli = APIClient(address, port, password) | ||||
|     stopping = False | ||||
|     retry_timer = [] | ||||
|  | ||||
|     has_connects = [] | ||||
|  | ||||
|     def try_connect(err, tries=0): | ||||
|         if stopping: | ||||
|             return | ||||
|  | ||||
|         if err: | ||||
|             _LOGGER.warning("Disconnected from API: %s", err) | ||||
|  | ||||
|         while retry_timer: | ||||
|             retry_timer.pop(0).cancel() | ||||
|  | ||||
|         error = None | ||||
|         try: | ||||
|             cli.connect() | ||||
|             cli.login() | ||||
|         except APIConnectionError as err2:  # noqa | ||||
|             error = err2 | ||||
|  | ||||
|         if error is None: | ||||
|             _LOGGER.info("Successfully connected to %s", address) | ||||
|             return | ||||
|  | ||||
|         wait_time = int(min(1.5 ** min(tries, 100), 30)) | ||||
|         if not has_connects: | ||||
|             _LOGGER.warning( | ||||
|                 "Initial connection failed. The ESP might not be connected " | ||||
|                 "to WiFi yet (%s). Re-Trying in %s seconds", | ||||
|                 error, | ||||
|                 wait_time, | ||||
|             ) | ||||
|         else: | ||||
|             _LOGGER.warning( | ||||
|                 "Couldn't connect to API (%s). Trying to reconnect in %s seconds", | ||||
|                 error, | ||||
|                 wait_time, | ||||
|             ) | ||||
|         timer = threading.Timer( | ||||
|             wait_time, functools.partial(try_connect, None, tries + 1) | ||||
|         ) | ||||
|         timer.start() | ||||
|         retry_timer.append(timer) | ||||
|  | ||||
|     def on_log(msg): | ||||
|         time_ = datetime.now().time().strftime("[%H:%M:%S]") | ||||
|         text = msg.message | ||||
|         if msg.send_failed: | ||||
|             text = color( | ||||
|                 Fore.WHITE, | ||||
|                 "(Message skipped because it was too big to fit in " | ||||
|                 "TCP buffer - This is only cosmetic)", | ||||
|             ) | ||||
|         safe_print(time_ + text) | ||||
|  | ||||
|     def on_login(): | ||||
|         try: | ||||
|             cli.subscribe_logs(on_log, dump_config=not has_connects) | ||||
|             has_connects.append(True) | ||||
|         except APIConnectionError: | ||||
|             cli.disconnect() | ||||
|  | ||||
|     cli.on_disconnect = try_connect | ||||
|     cli.on_login = on_login | ||||
|     cli.start() | ||||
|  | ||||
|     try: | ||||
|         try_connect(None) | ||||
|         while True: | ||||
|             time.sleep(1) | ||||
|     except KeyboardInterrupt: | ||||
|         stopping = True | ||||
|         cli.stop(True) | ||||
|         while retry_timer: | ||||
|             retry_timer.pop(0).cancel() | ||||
|     return 0 | ||||
| @@ -6,6 +6,7 @@ from esphome.const import ( | ||||
|     CONF_ELSE, | ||||
|     CONF_ID, | ||||
|     CONF_THEN, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_TYPE_ID, | ||||
|     CONF_TIME, | ||||
| @@ -244,6 +245,9 @@ def validate_wait_until(value): | ||||
|     schema = cv.Schema( | ||||
|         { | ||||
|             cv.Required(CONF_CONDITION): validate_potentially_and_condition, | ||||
|             cv.Optional(CONF_TIMEOUT): cv.templatable( | ||||
|                 cv.positive_time_period_milliseconds | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     if isinstance(value, dict) and CONF_CONDITION in value: | ||||
| @@ -255,6 +259,9 @@ def validate_wait_until(value): | ||||
| async def wait_until_action_to_code(config, action_id, template_arg, args): | ||||
|     conditions = await build_condition(config[CONF_CONDITION], template_arg, args) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, conditions) | ||||
|     if CONF_TIMEOUT in config: | ||||
|         template_ = await cg.templatable(config[CONF_TIMEOUT], args, cg.uint32) | ||||
|         cg.add(var.set_timeout_value(template_)) | ||||
|     await cg.register_component(var, {}) | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -30,6 +30,7 @@ from esphome.cpp_generator import (  # noqa | ||||
|     add_library, | ||||
|     add_build_flag, | ||||
|     add_define, | ||||
|     add_platformio_option, | ||||
|     get_variable, | ||||
|     get_variable_with_full_id, | ||||
|     process_lambda, | ||||
| @@ -60,12 +61,13 @@ from esphome.cpp_types import (  # noqa | ||||
|     uint8, | ||||
|     uint16, | ||||
|     uint32, | ||||
|     uint64, | ||||
|     int32, | ||||
|     const_char_ptr, | ||||
|     NAN, | ||||
|     esphome_ns, | ||||
|     App, | ||||
|     Nameable, | ||||
|     EntityBase, | ||||
|     Component, | ||||
|     ComponentPtr, | ||||
|     PollingComponent, | ||||
| @@ -77,4 +79,6 @@ from esphome.cpp_types import (  # noqa | ||||
|     JsonObjectConstRef, | ||||
|     Controller, | ||||
|     GPIOPin, | ||||
|     InternalGPIOPin, | ||||
|     gpio_Flags, | ||||
| ) | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/esphal.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/stepper/stepper.h" | ||||
|  | ||||
| namespace esphome { | ||||
|   | ||||
| @@ -1,10 +1,16 @@ | ||||
| #ifdef USE_ARDUINO | ||||
|  | ||||
| #include "ac_dimmer.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <cmath> | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
| #include <core_esp8266_waveform.h> | ||||
| #endif | ||||
| #ifdef USE_ESP32_FRAMEWORK_ARDUINO | ||||
| #include <esp32-hal-timer.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ac_dimmer { | ||||
| @@ -17,12 +23,15 @@ static AcDimmerDataStore *all_dimmers[32];  // NOLINT(cppcoreguidelines-avoid-no | ||||
| /// Time in microseconds the gate should be held high | ||||
| /// 10µs should be long enough for most triacs | ||||
| /// For reference: BT136 datasheet says 2µs nominal (page 7) | ||||
| static const uint32_t GATE_ENABLE_TIME = 10; | ||||
| /// However other factors like gate driver propagation time | ||||
| /// are also considered and a really low value is not important | ||||
| /// See also: https://github.com/esphome/issues/issues/1632 | ||||
| static const uint32_t GATE_ENABLE_TIME = 50; | ||||
|  | ||||
| /// Function called from timer interrupt | ||||
| /// Input is current time in microseconds (micros()) | ||||
| /// Returns when next "event" is expected in µs, or 0 if no such event known. | ||||
| uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { | ||||
| uint32_t IRAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { | ||||
|   // If no ZC signal received yet. | ||||
|   if (this->crossed_zero_at == 0) | ||||
|     return 0; | ||||
| @@ -34,13 +43,13 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { | ||||
|  | ||||
|   if (this->enable_time_us != 0 && time_since_zc >= this->enable_time_us) { | ||||
|     this->enable_time_us = 0; | ||||
|     this->gate_pin->digital_write(true); | ||||
|     this->gate_pin.digital_write(true); | ||||
|     // Prevent too short pulses | ||||
|     this->disable_time_us = max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); | ||||
|     this->disable_time_us = std::max(this->disable_time_us, time_since_zc + GATE_ENABLE_TIME); | ||||
|   } | ||||
|   if (this->disable_time_us != 0 && time_since_zc >= this->disable_time_us) { | ||||
|     this->disable_time_us = 0; | ||||
|     this->gate_pin->digital_write(false); | ||||
|     this->gate_pin.digital_write(false); | ||||
|   } | ||||
|  | ||||
|   if (time_since_zc < this->enable_time_us) | ||||
| @@ -60,7 +69,7 @@ uint32_t ICACHE_RAM_ATTR HOT AcDimmerDataStore::timer_intr(uint32_t now) { | ||||
| } | ||||
|  | ||||
| /// Run timer interrupt code and return in how many µs the next event is expected | ||||
| uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { | ||||
| uint32_t IRAM_ATTR HOT timer_interrupt() { | ||||
|   // run at least with 1kHz | ||||
|   uint32_t min_dt_us = 1000; | ||||
|   uint32_t now = micros(); | ||||
| @@ -77,7 +86,7 @@ uint32_t ICACHE_RAM_ATTR HOT timer_interrupt() { | ||||
| } | ||||
|  | ||||
| /// GPIO interrupt routine, called when ZC pin triggers | ||||
| void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
| void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|   uint32_t prev_crossed = this->crossed_zero_at; | ||||
|  | ||||
|   // 50Hz mains frequency should give a half cycle of 10ms a 60Hz will give 8.33ms | ||||
| @@ -94,7 +103,7 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|  | ||||
|   if (this->value == 65535) { | ||||
|     // fully on, enable output immediately | ||||
|     this->gate_pin->digital_write(true); | ||||
|     this->gate_pin.digital_write(true); | ||||
|   } else if (this->init_cycle) { | ||||
|     // send a full cycle | ||||
|     this->init_cycle = false; | ||||
| @@ -102,30 +111,30 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|     this->disable_time_us = cycle_time_us; | ||||
|   } else if (this->value == 0) { | ||||
|     // fully off, disable output immediately | ||||
|     this->gate_pin->digital_write(false); | ||||
|     this->gate_pin.digital_write(false); | ||||
|   } else { | ||||
|     if (this->method == DIM_METHOD_TRAILING) { | ||||
|       this->enable_time_us = 1;  // cannot be 0 | ||||
|       this->disable_time_us = max((uint32_t) 10, this->value * this->cycle_time_us / 65535); | ||||
|       this->disable_time_us = std::max((uint32_t) 10, this->value * this->cycle_time_us / 65535); | ||||
|     } else { | ||||
|       // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic | ||||
|       // also take into account min_power | ||||
|       auto min_us = this->cycle_time_us * this->min_power / 1000; | ||||
|       this->enable_time_us = 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) { | ||||
|         // 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->disable_time_us = max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); | ||||
|         this->disable_time_us = std::max(this->enable_time_us + GATE_ENABLE_TIME, (uint32_t) cycle_time_us / 10); | ||||
|       } else { | ||||
|         this->gate_pin->digital_write(false); | ||||
|         this->gate_pin.digital_write(false); | ||||
|         this->disable_time_us = this->cycle_time_us; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { | ||||
|   // Attaching pin interrupts on the same pin will override the previous interupt | ||||
| void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) { | ||||
|   // Attaching pin interrupts on the same pin will override the previous interrupt | ||||
|   // However, the user expects that multiple dimmers sharing the same ZC pin will work. | ||||
|   // We solve this in a bit of a hacky way: On each pin interrupt, we check all dimmers | ||||
|   // if any of them are using the same ZC pin, and also trigger the interrupt for *them*. | ||||
| @@ -138,11 +147,11 @@ void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #ifdef USE_ESP32 | ||||
| // ESP32 implementation, uses basically the same code but needs to wrap | ||||
| // timer_interrupt() function to auto-reschedule | ||||
| static hw_timer_t *dimmer_timer = nullptr; | ||||
| void ICACHE_RAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } | ||||
| static hw_timer_t *dimmer_timer = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); } | ||||
| #endif | ||||
|  | ||||
| void AcDimmer::setup() { | ||||
| @@ -171,15 +180,16 @@ void AcDimmer::setup() { | ||||
|   if (setup_zero_cross_pin) { | ||||
|     this->zero_cross_pin_->setup(); | ||||
|     this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr(); | ||||
|     this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, FALLING); | ||||
|     this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_, | ||||
|                                             gpio::INTERRUPT_FALLING_EDGE); | ||||
|   } | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
|   // Uses ESP8266 waveform (soft PWM) class | ||||
|   // PWM and AcDimmer can even run at the same time this way | ||||
|   setTimer1Callback(&timer_interrupt); | ||||
| #endif | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #ifdef USE_ESP32 | ||||
|   // 80 Divider -> 1 count=1µs | ||||
|   dimmer_timer = timerBegin(0, 80, true); | ||||
|   timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); | ||||
| @@ -215,3 +225,5 @@ void AcDimmer::dump_config() { | ||||
|  | ||||
| }  // namespace ac_dimmer | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ARDUINO | ||||
|   | ||||
| @@ -1,7 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/esphal.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/output/float_output.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -11,11 +13,11 @@ enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TR | ||||
|  | ||||
| struct AcDimmerDataStore { | ||||
|   /// Zero-cross pin | ||||
|   ISRInternalGPIOPin *zero_cross_pin; | ||||
|   ISRInternalGPIOPin zero_cross_pin; | ||||
|   /// Zero-cross pin number - used to share ZC pin across multiple dimmers | ||||
|   uint8_t zero_cross_pin_number; | ||||
|   /// Output pin to write to | ||||
|   ISRInternalGPIOPin *gate_pin; | ||||
|   ISRInternalGPIOPin gate_pin; | ||||
|   /// Value of the dimmer - 0 to 65535. | ||||
|   uint16_t value; | ||||
|   /// Minimum power for activation | ||||
| @@ -37,7 +39,7 @@ struct AcDimmerDataStore { | ||||
|  | ||||
|   void gpio_intr(); | ||||
|   static void s_gpio_intr(AcDimmerDataStore *store); | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #ifdef USE_ESP32 | ||||
|   static void s_timer_intr(); | ||||
| #endif | ||||
| }; | ||||
| @@ -47,16 +49,16 @@ class AcDimmer : public output::FloatOutput, public Component { | ||||
|   void setup() override; | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void set_gate_pin(GPIOPin *gate_pin) { gate_pin_ = gate_pin; } | ||||
|   void set_zero_cross_pin(GPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } | ||||
|   void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; } | ||||
|   void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; } | ||||
|   void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; } | ||||
|   void set_method(DimMethod method) { method_ = method; } | ||||
|  | ||||
|  protected: | ||||
|   void write_state(float state) override; | ||||
|  | ||||
|   GPIOPin *gate_pin_; | ||||
|   GPIOPin *zero_cross_pin_; | ||||
|   InternalGPIOPin *gate_pin_; | ||||
|   InternalGPIOPin *zero_cross_pin_; | ||||
|   AcDimmerDataStore store_; | ||||
|   bool init_with_half_cycle_; | ||||
|   DimMethod method_; | ||||
| @@ -64,3 +66,5 @@ class AcDimmer : public output::FloatOutput, public Component { | ||||
|  | ||||
| }  // namespace ac_dimmer | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ARDUINO | ||||
|   | ||||
| @@ -19,17 +19,20 @@ DIM_METHODS = { | ||||
| CONF_GATE_PIN = "gate_pin" | ||||
| CONF_ZERO_CROSS_PIN = "zero_cross_pin" | ||||
| CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle" | ||||
| CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_ID): cv.declare_id(AcDimmer), | ||||
|         cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, | ||||
|         cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, | ||||
|         cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, | ||||
|         cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum( | ||||
|             DIM_METHODS, upper=True, space="_" | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     output.FLOAT_OUTPUT_SCHEMA.extend( | ||||
|         { | ||||
|             cv.Required(CONF_ID): cv.declare_id(AcDimmer), | ||||
|             cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema, | ||||
|             cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema, | ||||
|             cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum( | ||||
|                 DIM_METHODS, upper=True, space="_" | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.only_with_arduino, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|   | ||||
| @@ -42,8 +42,9 @@ void AdalightLightEffect::reset_frame_(light::AddressableLight &it) { | ||||
|  | ||||
| void AdalightLightEffect::blank_all_leds_(light::AddressableLight &it) { | ||||
|   for (int led = it.size(); led-- > 0;) { | ||||
|     it[led].set(COLOR_BLACK); | ||||
|     it[led].set(Color::BLACK); | ||||
|   } | ||||
|   it.schedule_show(); | ||||
| } | ||||
|  | ||||
| void AdalightLightEffect::apply(light::AddressableLight &it, const Color ¤t_color) { | ||||
| @@ -133,6 +134,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL | ||||
|     it[led].set(Color(led_data[0], led_data[1], led_data[2], white)); | ||||
|   } | ||||
|  | ||||
|   it.schedule_show(); | ||||
|   return CONSUMED; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,13 @@ | ||||
| #include "adc_sensor.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP8266 | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
| #include <Esp.h> | ||||
| ADC_MODE(ADC_VCC) | ||||
| #else | ||||
| #include <Arduino.h> | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -10,44 +15,90 @@ namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| void ADCSensor::set_attenuation(adc_attenuation_t attenuation) { this->attenuation_ = attenuation; } | ||||
| #ifdef USE_ESP32 | ||||
| void ADCSensor::set_attenuation(adc_atten_t attenuation) { this->attenuation_ = attenuation; } | ||||
|  | ||||
| inline adc1_channel_t gpio_to_adc1(uint8_t pin) { | ||||
| #if CONFIG_IDF_TARGET_ESP32 | ||||
|   switch (pin) { | ||||
|     case 36: | ||||
|       return ADC1_CHANNEL_0; | ||||
|     case 37: | ||||
|       return ADC1_CHANNEL_1; | ||||
|     case 38: | ||||
|       return ADC1_CHANNEL_2; | ||||
|     case 39: | ||||
|       return ADC1_CHANNEL_3; | ||||
|     case 32: | ||||
|       return ADC1_CHANNEL_4; | ||||
|     case 33: | ||||
|       return ADC1_CHANNEL_5; | ||||
|     case 34: | ||||
|       return ADC1_CHANNEL_6; | ||||
|     case 35: | ||||
|       return ADC1_CHANNEL_7; | ||||
|     default: | ||||
|       return ADC1_CHANNEL_MAX; | ||||
|   } | ||||
| #elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 | ||||
|   switch (pin) { | ||||
|     case 0: | ||||
|       return ADC1_CHANNEL_0; | ||||
|     case 1: | ||||
|       return ADC1_CHANNEL_1; | ||||
|     case 2: | ||||
|       return ADC1_CHANNEL_2; | ||||
|     case 3: | ||||
|       return ADC1_CHANNEL_3; | ||||
|     case 4: | ||||
|       return ADC1_CHANNEL_4; | ||||
|     default: | ||||
|       return ADC1_CHANNEL_MAX; | ||||
|   } | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADC '%s'...", this->get_name().c_str()); | ||||
| #ifndef USE_ADC_SENSOR_VCC | ||||
|   GPIOPin(this->pin_, INPUT).setup(); | ||||
|   pin_->setup(); | ||||
| #endif | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
|   analogSetPinAttenuation(this->pin_, this->attenuation_); | ||||
| #ifdef USE_ESP32 | ||||
|   adc1_config_channel_atten(gpio_to_adc1(pin_->get_pin()), attenuation_); | ||||
|   adc1_config_width(ADC_WIDTH_BIT_12); | ||||
| #if !CONFIG_IDF_TARGET_ESP32C3 && !CONFIG_IDF_TARGET_ESP32H2 | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) gpio_to_adc1(pin_->get_pin())); | ||||
| #endif | ||||
| #endif | ||||
| } | ||||
| void ADCSensor::dump_config() { | ||||
|   LOG_SENSOR("", "ADC Sensor", this); | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: VCC"); | ||||
| #else | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_); | ||||
|   LOG_PIN("  Pin: ", pin_); | ||||
| #endif | ||||
| #endif | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
|   ESP_LOGCONFIG(TAG, "  Pin: %u", this->pin_); | ||||
| #ifdef USE_ESP32 | ||||
|   LOG_PIN("  Pin: ", pin_); | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_0db: | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|       break; | ||||
|     case ADC_2_5db: | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|       break; | ||||
|     case ADC_6db: | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|       break; | ||||
|     case ADC_11db: | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #endif | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| @@ -59,34 +110,56 @@ void ADCSensor::update() { | ||||
|   this->publish_state(value_v); | ||||
| } | ||||
| float ADCSensor::sample() { | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
|   float value_v = analogRead(this->pin_) / 4095.0f;  // NOLINT | ||||
| #ifdef USE_ESP32 | ||||
|   int raw = adc1_get_raw(gpio_to_adc1(pin_->get_pin())); | ||||
|   float value_v = raw / 4095.0f; | ||||
| #if CONFIG_IDF_TARGET_ESP32 | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_0db: | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       value_v *= 1.1; | ||||
|       break; | ||||
|     case ADC_2_5db: | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       value_v *= 1.5; | ||||
|       break; | ||||
|     case ADC_6db: | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       value_v *= 2.2; | ||||
|       break; | ||||
|     case ADC_11db: | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       value_v *= 3.9; | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #elif CONFIG_IDF_TARGET_ESP32C3 || CONFIG_IDF_TARGET_ESP32H2 | ||||
|   switch (this->attenuation_) { | ||||
|     case ADC_ATTEN_DB_0: | ||||
|       value_v *= 0.84; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_2_5: | ||||
|       value_v *= 1.13; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_6: | ||||
|       value_v *= 1.56; | ||||
|       break; | ||||
|     case ADC_ATTEN_DB_11: | ||||
|       value_v *= 3.0; | ||||
|       break; | ||||
|     default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|       break; | ||||
|   } | ||||
| #endif | ||||
|   return value_v; | ||||
| #endif | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
| #ifdef USE_ADC_SENSOR_VCC | ||||
|   return ESP.getVcc() / 1024.0f; | ||||
|   return ESP.getVcc() / 1024.0f;  // NOLINT(readability-static-accessed-through-instance) | ||||
| #else | ||||
|   return analogRead(this->pin_) / 1024.0f;  // NOLINT | ||||
|   return analogRead(this->pin_->get_pin()) / 1024.0f;  // NOLINT | ||||
| #endif | ||||
| #endif | ||||
| } | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
| std::string ADCSensor::unique_id() { return get_mac_address() + "-adc"; } | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -1,19 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/esphal.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/voltage_sampler/voltage_sampler.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #include "driver/adc.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage_sampler::VoltageSampler { | ||||
|  public: | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
| #ifdef USE_ESP32 | ||||
|   /// Set the attenuation for this pin. Only available on the ESP32. | ||||
|   void set_attenuation(adc_attenuation_t attenuation); | ||||
|   void set_attenuation(adc_atten_t attenuation); | ||||
| #endif | ||||
|  | ||||
|   /// Update adc values. | ||||
| @@ -23,18 +27,18 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage | ||||
|   void dump_config() override; | ||||
|   /// `HARDWARE_LATE` setup priority. | ||||
|   float get_setup_priority() const override; | ||||
|   void set_pin(uint8_t pin) { this->pin_ = pin; } | ||||
|   void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; } | ||||
|   float sample() override; | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP8266 | ||||
| #ifdef USE_ESP8266 | ||||
|   std::string unique_id() override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   uint8_t pin_; | ||||
|   InternalGPIOPin *pin_; | ||||
|  | ||||
| #ifdef ARDUINO_ARCH_ESP32 | ||||
|   adc_attenuation_t attenuation_{ADC_0db}; | ||||
| #ifdef USE_ESP32 | ||||
|   adc_atten_t attenuation_{ADC_ATTEN_DB_0}; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -5,29 +5,54 @@ from esphome.components import sensor, voltage_sampler | ||||
| from esphome.const import ( | ||||
|     CONF_ATTENUATION, | ||||
|     CONF_ID, | ||||
|     CONF_INPUT, | ||||
|     CONF_PIN, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_EMPTY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
|  | ||||
| AUTO_LOAD = ["voltage_sampler"] | ||||
|  | ||||
| ATTENUATION_MODES = { | ||||
|     "0db": cg.global_ns.ADC_0db, | ||||
|     "2.5db": cg.global_ns.ADC_2_5db, | ||||
|     "6db": cg.global_ns.ADC_6db, | ||||
|     "11db": cg.global_ns.ADC_11db, | ||||
|     "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, | ||||
| } | ||||
|  | ||||
|  | ||||
| def validate_adc_pin(value): | ||||
|     vcc = str(value).upper() | ||||
|     if vcc == "VCC": | ||||
|         return cv.only_on_esp8266(vcc) | ||||
|     return pins.analog_pin(value) | ||||
|     if str(value).upper() == "VCC": | ||||
|         return cv.only_on_esp8266("VCC") | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         from esphome.components.esp32 import is_esp32c3 | ||||
|  | ||||
|         value = pins.internal_gpio_input_pin_number(value) | ||||
|         if is_esp32c3(): | ||||
|             if not (0 <= value <= 4):  # ADC1 | ||||
|                 raise cv.Invalid("ESP32-C3: Only pins 0 though 4 support ADC.") | ||||
|         if not (32 <= value <= 39):  # ADC1 | ||||
|             raise cv.Invalid("ESP32: Only pins 32 though 39 support ADC.") | ||||
|     elif 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) | ||||
|     else: | ||||
|         raise NotImplementedError | ||||
|  | ||||
|     return pins.internal_gpio_input_pin_schema(value) | ||||
|  | ||||
|  | ||||
| adc_ns = cg.esphome_ns.namespace("adc") | ||||
| @@ -37,7 +62,10 @@ ADCSensor = adc_ns.class_( | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         UNIT_VOLT, ICON_EMPTY, 2, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=2, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
| @@ -60,7 +88,8 @@ async def to_code(config): | ||||
|     if config[CONF_PIN] == "VCC": | ||||
|         cg.add_define("USE_ADC_SENSOR_VCC") | ||||
|     else: | ||||
|         cg.add(var.set_pin(config[CONF_PIN])) | ||||
|         pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|         cg.add(var.set_pin(pin)) | ||||
|  | ||||
|     if CONF_ATTENUATION in config: | ||||
|         cg.add(var.set_attenuation(config[CONF_ATTENUATION])) | ||||
|   | ||||
| @@ -8,9 +8,7 @@ static const char *const TAG = "ade7953"; | ||||
|  | ||||
| void ADE7953::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "ADE7953:"); | ||||
|   if (this->has_irq_) { | ||||
|     ESP_LOGCONFIG(TAG, "  IRQ Pin: GPIO%u", this->irq_pin_number_); | ||||
|   } | ||||
|   LOG_PIN("  IRQ Pin: ", irq_pin_); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Voltage Sensor", this->voltage_sensor_); | ||||
| @@ -20,27 +18,28 @@ void ADE7953::dump_config() { | ||||
|   LOG_SENSOR("  ", "Active Power B Sensor", this->active_power_b_sensor_); | ||||
| } | ||||
|  | ||||
| #define ADE_PUBLISH_(name, factor) \ | ||||
|   if ((name) && this->name##_sensor_) { \ | ||||
|     float value = *(name) / (factor); \ | ||||
| #define ADE_PUBLISH_(name, val, factor) \ | ||||
|   if (err == i2c::ERROR_OK && this->name##_sensor_) { \ | ||||
|     float value = (val) / (factor); \ | ||||
|     this->name##_sensor_->publish_state(value); \ | ||||
|   } | ||||
| #define ADE_PUBLISH(name, factor) ADE_PUBLISH_(name, factor) | ||||
| #define ADE_PUBLISH(name, val, factor) ADE_PUBLISH_(name, val, factor) | ||||
|  | ||||
| void ADE7953::update() { | ||||
|   if (!this->is_setup_) | ||||
|     return; | ||||
|  | ||||
|   auto active_power_a = this->ade_read_<int32_t>(0x0312); | ||||
|   ADE_PUBLISH(active_power_a, 154.0f); | ||||
|   auto active_power_b = this->ade_read_<int32_t>(0x0313); | ||||
|   ADE_PUBLISH(active_power_b, 154.0f); | ||||
|   auto current_a = this->ade_read_<uint32_t>(0x031A); | ||||
|   ADE_PUBLISH(current_a, 100000.0f); | ||||
|   auto current_b = this->ade_read_<uint32_t>(0x031B); | ||||
|   ADE_PUBLISH(current_b, 100000.0f); | ||||
|   auto voltage = this->ade_read_<uint32_t>(0x031C); | ||||
|   ADE_PUBLISH(voltage, 26000.0f); | ||||
|   uint32_t val; | ||||
|   i2c::ErrorCode err = ade_read_32_(0x0312, &val); | ||||
|   ADE_PUBLISH(active_power_a, (int32_t) val, 154.0f); | ||||
|   err = ade_read_32_(0x0313, &val); | ||||
|   ADE_PUBLISH(active_power_b, (int32_t) val, 154.0f); | ||||
|   err = ade_read_32_(0x031A, &val); | ||||
|   ADE_PUBLISH(current_a, (uint32_t) val, 100000.0f); | ||||
|   err = ade_read_32_(0x031B, &val); | ||||
|   ADE_PUBLISH(current_b, (uint32_t) val, 100000.0f); | ||||
|   err = ade_read_32_(0x031C, &val); | ||||
|   ADE_PUBLISH(voltage, (uint32_t) val, 26000.0f); | ||||
|  | ||||
|   //    auto apparent_power_a = this->ade_read_<int32_t>(0x0310); | ||||
|   //    auto apparent_power_b = this->ade_read_<int32_t>(0x0311); | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| @@ -9,10 +10,7 @@ namespace ade7953 { | ||||
|  | ||||
| class ADE7953 : public i2c::I2CDevice, public PollingComponent { | ||||
|  public: | ||||
|   void set_irq_pin(uint8_t irq_pin) { | ||||
|     has_irq_ = true; | ||||
|     irq_pin_number_ = irq_pin; | ||||
|   } | ||||
|   void set_irq_pin(InternalGPIOPin *irq_pin) { irq_pin_ = irq_pin; } | ||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||
|   void set_current_a_sensor(sensor::Sensor *current_a_sensor) { current_a_sensor_ = current_a_sensor; } | ||||
|   void set_current_b_sensor(sensor::Sensor *current_b_sensor) { current_b_sensor_ = current_b_sensor; } | ||||
| @@ -24,15 +22,13 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { | ||||
|   } | ||||
|  | ||||
|   void setup() override { | ||||
|     if (this->has_irq_) { | ||||
|       auto pin = GPIOPin(this->irq_pin_number_, INPUT); | ||||
|       this->irq_pin_ = &pin; | ||||
|     if (this->irq_pin_ != nullptr) { | ||||
|       this->irq_pin_->setup(); | ||||
|     } | ||||
|     this->set_timeout(100, [this]() { | ||||
|       this->ade_write_<uint8_t>(0x0010, 0x04); | ||||
|       this->ade_write_<uint8_t>(0x00FE, 0xAD); | ||||
|       this->ade_write_<uint16_t>(0x0120, 0x0030); | ||||
|       this->ade_write_8_(0x0010, 0x04); | ||||
|       this->ade_write_8_(0x00FE, 0xAD); | ||||
|       this->ade_write_16_(0x0120, 0x0030); | ||||
|       this->is_setup_ = true; | ||||
|     }); | ||||
|   } | ||||
| @@ -42,31 +38,51 @@ class ADE7953 : public i2c::I2CDevice, public PollingComponent { | ||||
|   void update() override; | ||||
|  | ||||
|  protected: | ||||
|   template<typename T> bool ade_write_(uint16_t reg, T value) { | ||||
|   i2c::ErrorCode ade_write_8_(uint16_t reg, uint8_t value) { | ||||
|     std::vector<uint8_t> data; | ||||
|     data.push_back(reg >> 8); | ||||
|     data.push_back(reg >> 0); | ||||
|     for (int i = sizeof(T) - 1; i >= 0; i--) | ||||
|       data.push_back(value >> (i * 8)); | ||||
|     return this->write_bytes_raw(data); | ||||
|     data.push_back(value); | ||||
|     return write(data.data(), data.size()); | ||||
|   } | ||||
|   template<typename T> optional<T> ade_read_(uint16_t reg) { | ||||
|     uint8_t hi = reg >> 8; | ||||
|     uint8_t lo = reg >> 0; | ||||
|     if (!this->write_bytes_raw({hi, lo})) | ||||
|       return {}; | ||||
|     auto ret = this->read_bytes_raw<sizeof(T)>(); | ||||
|     if (!ret.has_value()) | ||||
|       return {}; | ||||
|     T result = 0; | ||||
|     for (int i = 0, j = sizeof(T) - 1; i < sizeof(T); i++, j--) | ||||
|       result |= T((*ret)[i]) << (j * 8); | ||||
|     return result; | ||||
|   i2c::ErrorCode ade_write_16_(uint16_t reg, uint16_t value) { | ||||
|     std::vector<uint8_t> data; | ||||
|     data.push_back(reg >> 8); | ||||
|     data.push_back(reg >> 0); | ||||
|     data.push_back(value >> 8); | ||||
|     data.push_back(value >> 0); | ||||
|     return write(data.data(), data.size()); | ||||
|   } | ||||
|   i2c::ErrorCode ade_write_32_(uint16_t reg, uint32_t value) { | ||||
|     std::vector<uint8_t> data; | ||||
|     data.push_back(reg >> 8); | ||||
|     data.push_back(reg >> 0); | ||||
|     data.push_back(value >> 24); | ||||
|     data.push_back(value >> 16); | ||||
|     data.push_back(value >> 8); | ||||
|     data.push_back(value >> 0); | ||||
|     return write(data.data(), data.size()); | ||||
|   } | ||||
|   i2c::ErrorCode ade_read_32_(uint16_t reg, uint32_t *value) { | ||||
|     uint8_t reg_data[2]; | ||||
|     reg_data[0] = reg >> 8; | ||||
|     reg_data[1] = reg >> 0; | ||||
|     i2c::ErrorCode err = write(reg_data, 2); | ||||
|     if (err != i2c::ERROR_OK) | ||||
|       return err; | ||||
|     uint8_t recv[4]; | ||||
|     err = read(recv, 4); | ||||
|     if (err != i2c::ERROR_OK) | ||||
|       return err; | ||||
|     *value = 0; | ||||
|     *value |= ((uint32_t) recv[0]) << 24; | ||||
|     *value |= ((uint32_t) recv[1]) << 24; | ||||
|     *value |= ((uint32_t) recv[2]) << 24; | ||||
|     *value |= ((uint32_t) recv[3]) << 24; | ||||
|     return i2c::ERROR_OK; | ||||
|   } | ||||
|  | ||||
|   bool has_irq_ = false; | ||||
|   uint8_t irq_pin_number_; | ||||
|   GPIOPin *irq_pin_{nullptr}; | ||||
|   InternalGPIOPin *irq_pin_ = nullptr; | ||||
|   bool is_setup_{false}; | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_a_sensor_{nullptr}; | ||||
|   | ||||
| @@ -8,7 +8,6 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_EMPTY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
|     UNIT_AMPERE, | ||||
| @@ -30,29 +29,36 @@ CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ADE7953), | ||||
|             cv.Optional(CONF_IRQ_PIN): pins.input_pin, | ||||
|             cv.Optional(CONF_IRQ_PIN): pins.internal_gpio_input_pin_schema, | ||||
|             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( | ||||
|                 UNIT_VOLT, ICON_EMPTY, 1, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT | ||||
|                 unit_of_measurement=UNIT_VOLT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_VOLTAGE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CURRENT_A): sensor.sensor_schema( | ||||
|                 UNIT_AMPERE, | ||||
|                 ICON_EMPTY, | ||||
|                 2, | ||||
|                 DEVICE_CLASS_CURRENT, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_AMPERE, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_CURRENT, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CURRENT_B): sensor.sensor_schema( | ||||
|                 UNIT_AMPERE, | ||||
|                 ICON_EMPTY, | ||||
|                 2, | ||||
|                 DEVICE_CLASS_CURRENT, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_AMPERE, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_CURRENT, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTIVE_POWER_A): sensor.sensor_schema( | ||||
|                 UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||
|                 unit_of_measurement=UNIT_WATT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ACTIVE_POWER_B): sensor.sensor_schema( | ||||
|                 UNIT_WATT, ICON_EMPTY, 1, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT | ||||
|                 unit_of_measurement=UNIT_WATT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
| @@ -67,7 +73,8 @@ async def to_code(config): | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if CONF_IRQ_PIN in config: | ||||
|         cg.add(var.set_irq_pin(config[CONF_IRQ_PIN])) | ||||
|         irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN]) | ||||
|         cg.add(var.set_irq_pin(irq_pin)) | ||||
|  | ||||
|     for key in [ | ||||
|         CONF_VOLTAGE, | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "ads1115.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ads1115 { | ||||
| @@ -64,11 +65,6 @@ void ADS1115Component::setup() { | ||||
|     return; | ||||
|   } | ||||
|   this->prev_config_ = config; | ||||
|  | ||||
|   for (auto *sensor : this->sensors_) { | ||||
|     this->set_interval(sensor->get_name(), sensor->update_interval(), | ||||
|                        [this, sensor] { this->request_measurement(sensor); }); | ||||
|   } | ||||
| } | ||||
| void ADS1115Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up ADS1115..."); | ||||
| @@ -107,17 +103,22 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | ||||
|     } | ||||
|     this->prev_config_ = config; | ||||
|  | ||||
|     // about 1.6 ms with 860 samples per second | ||||
|     // about 1.2 ms with 860 samples per second | ||||
|     delay(2); | ||||
|  | ||||
|     uint32_t start = millis(); | ||||
|     while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { | ||||
|       if (millis() - start > 100) { | ||||
|         ESP_LOGW(TAG, "Reading ADS1115 timed out"); | ||||
|         this->status_set_warning(); | ||||
|         return NAN; | ||||
|     // in continuous mode, conversion will always be running, rely on the delay | ||||
|     // to ensure conversion is taking place with the correct settings | ||||
|     // can we use the rdy pin to trigger when a conversion is done? | ||||
|     if (!this->continuous_mode_) { | ||||
|       uint32_t start = millis(); | ||||
|       while (this->read_byte_16(ADS1115_REGISTER_CONFIG, &config) && (config >> 15) == 0) { | ||||
|         if (millis() - start > 100) { | ||||
|           ESP_LOGW(TAG, "Reading ADS1115 timed out"); | ||||
|           this->status_set_warning(); | ||||
|           return NAN; | ||||
|         } | ||||
|         yield(); | ||||
|       } | ||||
|       yield(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -159,7 +160,7 @@ float ADS1115Component::request_measurement(ADS1115Sensor *sensor) { | ||||
| float ADS1115Sensor::sample() { return this->parent_->request_measurement(this); } | ||||
| void ADS1115Sensor::update() { | ||||
|   float v = this->parent_->request_measurement(this); | ||||
|   if (!isnan(v)) { | ||||
|   if (!std::isnan(v)) { | ||||
|     ESP_LOGD(TAG, "'%s': Got Voltage=%fV", this->get_name().c_str(), v); | ||||
|     this->publish_state(v); | ||||
|   } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from esphome.const import ( | ||||
|     CONF_GAIN, | ||||
|     CONF_MULTIPLEXER, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_EMPTY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_VOLT, | ||||
|     CONF_ID, | ||||
| @@ -53,7 +52,10 @@ ADS1115Sensor = ads1115_ns.class_( | ||||
| CONF_ADS1115_ID = "ads1115_id" | ||||
| CONFIG_SCHEMA = ( | ||||
|     sensor.sensor_schema( | ||||
|         UNIT_VOLT, ICON_EMPTY, 3, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=3, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ) | ||||
|     .extend( | ||||
|         { | ||||
|   | ||||
| @@ -10,10 +10,11 @@ | ||||
| // | ||||
| // According to the datasheet, the component is supposed to respond in more than 75ms. In fact, it can answer almost | ||||
| // immediately for temperature. But for humidity, it takes >90ms to get a valid data. From experience, we have best | ||||
| // results making successive requests; the current implementation make 3 attemps with a delay of 30ms each time. | ||||
| // results making successive requests; the current implementation makes 3 attempts with a delay of 30ms each time. | ||||
|  | ||||
| #include "aht10.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace aht10 { | ||||
| @@ -23,7 +24,7 @@ static const uint8_t AHT10_CALIBRATE_CMD[] = {0xE1}; | ||||
| static const uint8_t AHT10_MEASURE_CMD[] = {0xAC, 0x33, 0x00}; | ||||
| static const uint8_t AHT10_DEFAULT_DELAY = 5;    // ms, for calibration and temperature measurement | ||||
| static const uint8_t AHT10_HUMIDITY_DELAY = 30;  // ms | ||||
| static const uint8_t AHT10_ATTEMPS = 3;          // safety margin, normally 3 attemps are enough: 3*30=90ms | ||||
| static const uint8_t AHT10_ATTEMPTS = 3;         // safety margin, normally 3 attempts are enough: 3*30=90ms | ||||
|  | ||||
| void AHT10Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up AHT10..."); | ||||
| @@ -33,8 +34,19 @@ void AHT10Component::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   uint8_t data; | ||||
|   if (!this->read_byte(0, &data, AHT10_DEFAULT_DELAY)) { | ||||
|   uint8_t data = 0; | ||||
|   if (this->write(&data, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   delay(AHT10_DEFAULT_DELAY); | ||||
|   if (this->read(&data, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (this->read(&data, 1) != i2c::ERROR_OK) { | ||||
|     ESP_LOGD(TAG, "Communication with AHT10 failed!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
| @@ -55,15 +67,26 @@ void AHT10Component::update() { | ||||
|     return; | ||||
|   } | ||||
|   uint8_t data[6]; | ||||
|   uint8_t delay = AHT10_DEFAULT_DELAY; | ||||
|   uint8_t delay_ms = AHT10_DEFAULT_DELAY; | ||||
|   if (this->humidity_sensor_ != nullptr) | ||||
|     delay = AHT10_HUMIDITY_DELAY; | ||||
|   for (int i = 0; i < AHT10_ATTEMPS; ++i) { | ||||
|     ESP_LOGVV(TAG, "Attemps %u at %6ld", i, millis()); | ||||
|     delay_ms = AHT10_HUMIDITY_DELAY; | ||||
|   bool success = false; | ||||
|   for (int i = 0; i < AHT10_ATTEMPTS; ++i) { | ||||
|     ESP_LOGVV(TAG, "Attempt %d at %6u", i, millis()); | ||||
|     delay_microseconds_accurate(4); | ||||
|     if (!this->read_bytes(0, data, 6, delay)) { | ||||
|  | ||||
|     uint8_t reg = 0; | ||||
|     if (this->write(®, 1) != i2c::ERROR_OK) { | ||||
|       ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); | ||||
|     } else if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy | ||||
|       continue; | ||||
|     } | ||||
|     delay(delay_ms); | ||||
|     if (this->read(data, 6) != i2c::ERROR_OK) { | ||||
|       ESP_LOGD(TAG, "Communication with AHT10 failed, waiting..."); | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if ((data[0] & 0x80) == 0x80) {  // Bit[7] = 0b1, device is busy | ||||
|       ESP_LOGD(TAG, "AHT10 is busy, waiting..."); | ||||
|     } else if (data[1] == 0x0 && data[2] == 0x0 && (data[3] >> 4) == 0x0) { | ||||
|       // Unrealistic humidity (0x0) | ||||
| @@ -80,11 +103,12 @@ void AHT10Component::update() { | ||||
|       } | ||||
|     } else { | ||||
|       // data is valid, we can break the loop | ||||
|       ESP_LOGVV(TAG, "Answer at %6ld", millis()); | ||||
|       ESP_LOGVV(TAG, "Answer at %6u", millis()); | ||||
|       success = true; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if ((data[0] & 0x80) == 0x80) { | ||||
|   if (!success || (data[0] & 0x80) == 0x80) { | ||||
|     ESP_LOGE(TAG, "Measurements reading timed-out!"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
| @@ -105,7 +129,7 @@ void AHT10Component::update() { | ||||
|     this->temperature_sensor_->publish_state(temperature); | ||||
|   } | ||||
|   if (this->humidity_sensor_ != nullptr) { | ||||
|     if (isnan(humidity)) | ||||
|     if (std::isnan(humidity)) | ||||
|       ESP_LOGW(TAG, "Invalid humidity! Sensor reported 0%% Hum"); | ||||
|     this->humidity_sensor_->publish_state(humidity); | ||||
|   } | ||||
|   | ||||
| @@ -7,7 +7,6 @@ from esphome.const import ( | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ICON_EMPTY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| @@ -23,18 +22,16 @@ CONFIG_SCHEMA = ( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AHT10Component), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 UNIT_CELSIUS, | ||||
|                 ICON_EMPTY, | ||||
|                 2, | ||||
|                 DEVICE_CLASS_TEMPERATURE, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, | ||||
|                 ICON_EMPTY, | ||||
|                 2, | ||||
|                 DEVICE_CLASS_HUMIDITY, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/airthings_ble/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import esp32_ble_tracker | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| DEPENDENCIES = ["esp32_ble_tracker"] | ||||
| CODEOWNERS = ["@jeromelaban"] | ||||
|  | ||||
| airthings_ble_ns = cg.esphome_ns.namespace("airthings_ble") | ||||
| AirthingsListener = airthings_ble_ns.class_( | ||||
|     "AirthingsListener", esp32_ble_tracker.ESPBTDeviceListener | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(AirthingsListener), | ||||
|     } | ||||
| ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield esp32_ble_tracker.register_ble_device(var, config) | ||||
							
								
								
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								esphome/components/airthings_ble/airthings_listener.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,33 @@ | ||||
| #include "airthings_listener.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_ble { | ||||
|  | ||||
| static const char *const TAG = "airthings_ble"; | ||||
|  | ||||
| bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   for (auto &it : device.get_manufacturer_datas()) { | ||||
|     if (it.uuid == esp32_ble_tracker::ESPBTUUID::from_uint32(0x0334)) { | ||||
|       if (it.data.size() < 4) | ||||
|         continue; | ||||
|  | ||||
|       uint32_t sn = it.data[0]; | ||||
|       sn |= ((uint32_t) it.data[1] << 8); | ||||
|       sn |= ((uint32_t) it.data[2] << 16); | ||||
|       sn |= ((uint32_t) it.data[3] << 24); | ||||
|  | ||||
|       ESP_LOGD(TAG, "Found AirThings device Serial:%u (MAC: %s)", sn, device.address_str().c_str()); | ||||
|       return true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| }  // namespace airthings_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										19
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/airthings_ble/airthings_listener.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_ble { | ||||
|  | ||||
| class AirthingsListener : public esp32_ble_tracker::ESPBTDeviceListener { | ||||
|  public: | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace airthings_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										1
									
								
								esphome/components/airthings_wave_mini/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_mini/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@ncareau"] | ||||
							
								
								
									
										113
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,113 @@ | ||||
| #include "airthings_wave_mini.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_mini { | ||||
|  | ||||
| static const char *const TAG = "airthings_wave_mini"; | ||||
|  | ||||
| void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                             esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status == ESP_GATT_OK) { | ||||
|         ESP_LOGI(TAG, "Connected successfully!"); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGW(TAG, "Disconnected!"); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->handle_ = chr->handle; | ||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||
|  | ||||
|       request_read_values_(); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent()->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->handle_) { | ||||
|         read_sensors_(param->read.value, param->read.value_len); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto value = (WaveMiniReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WaveMiniReadings) <= value_len) { | ||||
|     this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||
|     this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|     this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); | ||||
|     if (is_valid_voc_value_(value->voc)) { | ||||
|       this->tvoc_sensor_->publish_state(value->voc); | ||||
|     } | ||||
|  | ||||
|     // This instance must not stay connected | ||||
|     // so other clients can connect to it (e.g. the | ||||
|     // mobile app). | ||||
|     parent()->set_enabled(false); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||
|  | ||||
| void AirthingsWaveMini::update() { | ||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||
|     if (!parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "Reconnecting to device"); | ||||
|       parent()->set_enabled(true); | ||||
|       parent()->connect(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Connection in progress"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::request_read_values_() { | ||||
|   auto status = | ||||
|       esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWaveMini::dump_config() { | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWaveMini::AirthingsWaveMini() | ||||
|     : PollingComponent(10000), | ||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), | ||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | ||||
|  | ||||
| }  // namespace airthings_wave_mini | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										65
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								esphome/components/airthings_wave_mini/airthings_wave_mini.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| #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_mini { | ||||
|  | ||||
| static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { | ||||
|  public: | ||||
|   AirthingsWaveMini(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_voc_value_(uint16_t voc); | ||||
|  | ||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); | ||||
|   void request_read_values_(); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|   sensor::Sensor *pressure_sensor_{nullptr}; | ||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||
|  | ||||
|   uint16_t handle_; | ||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||
|  | ||||
|   struct WaveMiniReadings { | ||||
|     uint16_t unused01; | ||||
|     uint16_t temperature; | ||||
|     uint16_t pressure; | ||||
|     uint16_t humidity; | ||||
|     uint16_t voc; | ||||
|     uint16_t unused02; | ||||
|     uint32_t unused03; | ||||
|     uint32_t unused04; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| }  // namespace airthings_wave_mini | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										82
									
								
								esphome/components/airthings_wave_mini/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								esphome/components/airthings_wave_mini/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,82 @@ | ||||
| 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_ID, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TVOC, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     UNIT_PARTS_PER_BILLION, | ||||
|     ICON_RADIATOR, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") | ||||
| AirthingsWaveMini = airthings_wave_mini_ns.class_( | ||||
|     "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AirthingsWaveMini), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 accuracy_decimals=2, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||
|                 icon=ICON_RADIATOR, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5min")) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure(sens)) | ||||
|     if CONF_TVOC in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||
|         cg.add(var.set_tvoc(sens)) | ||||
							
								
								
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/airthings_wave_plus/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@jeromelaban"] | ||||
							
								
								
									
										137
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,137 @@ | ||||
| #include "airthings_wave_plus.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace airthings_wave_plus { | ||||
|  | ||||
| static const char *const TAG = "airthings_wave_plus"; | ||||
|  | ||||
| void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                             esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       if (param->open.status == ESP_GATT_OK) { | ||||
|         ESP_LOGI(TAG, "Connected successfully!"); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGW(TAG, "Disconnected!"); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       this->handle_ = 0; | ||||
|       auto chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), | ||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->handle_ = chr->handle; | ||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||
|  | ||||
|       request_read_values_(); | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent()->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->handle_) { | ||||
|         read_sensors_(param->read.value, param->read.value_len); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | ||||
|   auto value = (WavePlusReadings *) raw_value; | ||||
|  | ||||
|   if (sizeof(WavePlusReadings) <= value_len) { | ||||
|     ESP_LOGD(TAG, "version = %d", value->version); | ||||
|  | ||||
|     if (value->version == 1) { | ||||
|       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||
|  | ||||
|       this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||
|       if (is_valid_radon_value_(value->radon)) { | ||||
|         this->radon_sensor_->publish_state(value->radon); | ||||
|       } | ||||
|       if (is_valid_radon_value_(value->radon_lt)) { | ||||
|         this->radon_long_term_sensor_->publish_state(value->radon_lt); | ||||
|       } | ||||
|       this->temperature_sensor_->publish_state(value->temperature / 100.0f); | ||||
|       this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||
|       if (is_valid_co2_value_(value->co2)) { | ||||
|         this->co2_sensor_->publish_state(value->co2); | ||||
|       } | ||||
|       if (is_valid_voc_value_(value->voc)) { | ||||
|         this->tvoc_sensor_->publish_state(value->voc); | ||||
|       } | ||||
|  | ||||
|       // This instance must not stay connected | ||||
|       // so other clients can connect to it (e.g. the | ||||
|       // mobile app). | ||||
|       parent()->set_enabled(false); | ||||
|     } else { | ||||
|       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||
|  | ||||
| bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } | ||||
|  | ||||
| void AirthingsWavePlus::update() { | ||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||
|     if (!parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "Reconnecting to device"); | ||||
|       parent()->set_enabled(true); | ||||
|       parent()->connect(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Connection in progress"); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::request_read_values_() { | ||||
|   auto status = | ||||
|       esp_ble_gattc_read_char(this->parent()->gattc_if, this->parent()->conn_id, this->handle_, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void AirthingsWavePlus::dump_config() { | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||
|   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||
| } | ||||
|  | ||||
| AirthingsWavePlus::AirthingsWavePlus() | ||||
|     : PollingComponent(10000), | ||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), | ||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | ||||
|  | ||||
| }  // namespace airthings_wave_plus | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										75
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								esphome/components/airthings_wave_plus/airthings_wave_plus.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| #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_plus { | ||||
|  | ||||
| static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | ||||
| static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; | ||||
|  | ||||
| class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { | ||||
|  public: | ||||
|   AirthingsWavePlus(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||
|   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } | ||||
|   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||
|   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||
|  | ||||
|  protected: | ||||
|   bool is_valid_radon_value_(uint16_t radon); | ||||
|   bool is_valid_voc_value_(uint16_t voc); | ||||
|   bool is_valid_co2_value_(uint16_t co2); | ||||
|  | ||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); | ||||
|   void request_read_values_(); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_sensor_{nullptr}; | ||||
|   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
|   sensor::Sensor *pressure_sensor_{nullptr}; | ||||
|   sensor::Sensor *co2_sensor_{nullptr}; | ||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||
|  | ||||
|   uint16_t handle_; | ||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||
|  | ||||
|   struct WavePlusReadings { | ||||
|     uint8_t version; | ||||
|     uint8_t humidity; | ||||
|     uint8_t ambientLight; | ||||
|     uint8_t unused01; | ||||
|     uint16_t radon; | ||||
|     uint16_t radon_lt; | ||||
|     uint16_t temperature; | ||||
|     uint16_t pressure; | ||||
|     uint16_t co2; | ||||
|     uint16_t voc; | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| }  // namespace airthings_wave_plus | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/airthings_wave_plus/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client | ||||
|  | ||||
| from esphome.const import ( | ||||
|     DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_PRESSURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_PERCENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HECTOPASCAL, | ||||
|     ICON_RADIOACTIVE, | ||||
|     CONF_ID, | ||||
|     CONF_RADON, | ||||
|     CONF_RADON_LONG_TERM, | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_TVOC, | ||||
|     CONF_CO2, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|     UNIT_PARTS_PER_MILLION, | ||||
|     UNIT_PARTS_PER_BILLION, | ||||
|     ICON_RADIATOR, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | ||||
| AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||
|     "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode | ||||
| ) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|                 accuracy_decimals=0, | ||||
|             ), | ||||
|             cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_RADON_LONG_TERM): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||
|                 icon=ICON_RADIOACTIVE, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||
|                 icon=ICON_RADIATOR, | ||||
|                 accuracy_decimals=0, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("5min")) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_HUMIDITY in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||
|         cg.add(var.set_humidity(sens)) | ||||
|     if CONF_RADON in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_RADON]) | ||||
|         cg.add(var.set_radon(sens)) | ||||
|     if CONF_RADON_LONG_TERM in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) | ||||
|         cg.add(var.set_radon_long_term(sens)) | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature(sens)) | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure(sens)) | ||||
|     if CONF_CO2 in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_CO2]) | ||||
|         cg.add(var.set_co2(sens)) | ||||
|     if CONF_TVOC in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||
|         cg.add(var.set_tvoc(sens)) | ||||
| @@ -5,6 +5,7 @@ | ||||
|  | ||||
| #include "am2320.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am2320 { | ||||
| @@ -77,7 +78,7 @@ bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len | ||||
|  | ||||
|   if (conversion > 0) | ||||
|     delay(conversion); | ||||
|   return this->parent_->raw_receive(this->address_, data, len); | ||||
|   return this->read(data, len) == i2c::ERROR_OK; | ||||
| } | ||||
|  | ||||
| bool AM2320Component::read_data_(uint8_t *data) { | ||||
|   | ||||
| @@ -9,7 +9,6 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     ICON_EMPTY, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| @@ -25,18 +24,16 @@ CONFIG_SCHEMA = ( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(AM2320Component), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 UNIT_CELSIUS, | ||||
|                 ICON_EMPTY, | ||||
|                 1, | ||||
|                 DEVICE_CLASS_TEMPERATURE, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, | ||||
|                 ICON_EMPTY, | ||||
|                 1, | ||||
|                 DEVICE_CLASS_HUMIDITY, | ||||
|                 STATE_CLASS_MEASUREMENT, | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										116
									
								
								esphome/components/am43/am43.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								esphome/components/am43/am43.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| #include "am43.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| static const char *const TAG = "am43"; | ||||
|  | ||||
| void Am43::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "AM43"); | ||||
|   LOG_SENSOR(" ", "Battery", this->battery_); | ||||
|   LOG_SENSOR(" ", "Illuminance", this->illuminance_); | ||||
| } | ||||
|  | ||||
| void Am43::setup() { | ||||
|   this->encoder_ = make_unique<Am43Encoder>(); | ||||
|   this->decoder_ = make_unique<Am43Decoder>(); | ||||
|   this->logged_in_ = false; | ||||
|   this->last_battery_update_ = 0; | ||||
|   this->current_sensor_ = 0; | ||||
| } | ||||
|  | ||||
| void Am43::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: { | ||||
|       this->logged_in_ = false; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       this->logged_in_ = false; | ||||
|       this->node_state = espbt::ClientState::IDLE; | ||||
|       if (this->battery_ != nullptr) | ||||
|         this->battery_->publish_state(NAN); | ||||
|       if (this->illuminance_ != nullptr) | ||||
|         this->illuminance_->publish_state(NAN); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { | ||||
|           ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", | ||||
|                    this->parent_->address_str().c_str()); | ||||
|         } else { | ||||
|           ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", | ||||
|                    this->parent_->address_str().c_str()); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       this->char_handle_ = chr->handle; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|       this->update(); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (param->notify.handle != this->char_handle_) | ||||
|         break; | ||||
|       this->decoder_->decode(param->notify.value, param->notify.value_len); | ||||
|  | ||||
|       if (this->battery_ != nullptr && this->decoder_->has_battery_level() && | ||||
|           millis() - this->last_battery_update_ > 10000) { | ||||
|         this->battery_->publish_state(this->decoder_->battery_level_); | ||||
|         this->last_battery_update_ = millis(); | ||||
|       } | ||||
|  | ||||
|       if (this->illuminance_ != nullptr && this->decoder_->has_light_level()) { | ||||
|         this->illuminance_->publish_state(this->decoder_->light_level_); | ||||
|       } | ||||
|  | ||||
|       if (this->current_sensor_ > 0) { | ||||
|         if (this->illuminance_ != nullptr) { | ||||
|           auto packet = this->encoder_->get_light_level_request(); | ||||
|           auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                                  packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                                  ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) | ||||
|             ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), | ||||
|                      status); | ||||
|         } | ||||
|         this->current_sensor_ = 0; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Am43::update() { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (this->current_sensor_ == 0) { | ||||
|     if (this->battery_ != nullptr) { | ||||
|       auto packet = this->encoder_->get_battery_level_request(); | ||||
|       auto status = | ||||
|           esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                    packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|       if (status) | ||||
|         ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } | ||||
|     this->current_sensor_++; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										45
									
								
								esphome/components/am43/am43.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/am43/am43.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #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/components/am43/am43_base.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||
|  public: | ||||
|   void setup() 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 dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   void set_battery(sensor::Sensor *battery) { battery_ = battery; } | ||||
|   void set_illuminance(sensor::Sensor *illuminance) { illuminance_ = illuminance; } | ||||
|  | ||||
|  protected: | ||||
|   uint16_t char_handle_; | ||||
|   std::unique_ptr<Am43Encoder> encoder_; | ||||
|   std::unique_ptr<Am43Decoder> decoder_; | ||||
|   bool logged_in_; | ||||
|   sensor::Sensor *battery_{nullptr}; | ||||
|   sensor::Sensor *illuminance_{nullptr}; | ||||
|   uint8_t current_sensor_; | ||||
|   // The AM43 often gets into a state where it spams loads of battery update | ||||
|   // notifications. Here we will limit to no more than every 10s. | ||||
|   uint8_t last_battery_update_; | ||||
| }; | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										144
									
								
								esphome/components/am43/am43_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								esphome/components/am43/am43_base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,144 @@ | ||||
| #include "am43_base.h" | ||||
| #include <cstring> | ||||
| #include <cstdio> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| const uint8_t START_PACKET[5] = {0x00, 0xff, 0x00, 0x00, 0x9a}; | ||||
|  | ||||
| std::string pkt_to_hex(const uint8_t *data, uint16_t len) { | ||||
|   char buf[64]; | ||||
|   memset(buf, 0, 64); | ||||
|   for (int i = 0; i < len; i++) | ||||
|     sprintf(&buf[i * 2], "%02x", data[i]); | ||||
|   std::string ret = buf; | ||||
|   return ret; | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_battery_level_request() { | ||||
|   uint8_t data = 0x1; | ||||
|   return this->encode_(0xA2, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_light_level_request() { | ||||
|   uint8_t data = 0x1; | ||||
|   return this->encode_(0xAA, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_position_request() { | ||||
|   uint8_t data = 0x1; | ||||
|   return this->encode_(CMD_GET_POSITION, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_send_pin_request(uint16_t pin) { | ||||
|   uint8_t data[2]; | ||||
|   data[0] = (pin & 0xFF00) >> 8; | ||||
|   data[1] = pin & 0xFF; | ||||
|   return this->encode_(CMD_SEND_PIN, data, 2); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_open_request() { | ||||
|   uint8_t data = 0xDD; | ||||
|   return this->encode_(CMD_SET_STATE, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_close_request() { | ||||
|   uint8_t data = 0xEE; | ||||
|   return this->encode_(CMD_SET_STATE, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_stop_request() { | ||||
|   uint8_t data = 0xCC; | ||||
|   return this->encode_(CMD_SET_STATE, &data, 1); | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::get_set_position_request(uint8_t position) { | ||||
|   return this->encode_(CMD_SET_POSITION, &position, 1); | ||||
| } | ||||
|  | ||||
| void Am43Encoder::checksum_() { | ||||
|   uint8_t checksum = 0; | ||||
|   int i = 0; | ||||
|   for (i = 0; i < this->packet_.length; i++) | ||||
|     checksum = checksum ^ this->packet_.data[i]; | ||||
|   this->packet_.data[i] = checksum ^ 0xff; | ||||
|   this->packet_.length++; | ||||
| } | ||||
|  | ||||
| Am43Packet *Am43Encoder::encode_(uint8_t command, uint8_t *data, uint8_t length) { | ||||
|   memcpy(this->packet_.data, START_PACKET, 5); | ||||
|   this->packet_.data[5] = command; | ||||
|   this->packet_.data[6] = length; | ||||
|   memcpy(&this->packet_.data[7], data, length); | ||||
|   this->packet_.length = length + 7; | ||||
|   this->checksum_(); | ||||
|   ESP_LOGV("am43", "ENC(%d): 0x%s", packet_.length, pkt_to_hex(packet_.data, packet_.length).c_str()); | ||||
|   return &this->packet_; | ||||
| } | ||||
|  | ||||
| #define VERIFY_MIN_LENGTH(x) \ | ||||
|   if (length < (x)) \ | ||||
|     return; | ||||
|  | ||||
| void Am43Decoder::decode(const uint8_t *data, uint16_t length) { | ||||
|   this->has_battery_level_ = false; | ||||
|   this->has_light_level_ = false; | ||||
|   this->has_set_position_response_ = false; | ||||
|   this->has_set_state_response_ = false; | ||||
|   this->has_position_ = false; | ||||
|   this->has_pin_response_ = false; | ||||
|   ESP_LOGV("am43", "DEC(%d): 0x%s", length, pkt_to_hex(data, length).c_str()); | ||||
|  | ||||
|   if (length < 2 || data[0] != 0x9a) | ||||
|     return; | ||||
|   switch (data[1]) { | ||||
|     case CMD_GET_BATTERY_LEVEL: { | ||||
|       VERIFY_MIN_LENGTH(8); | ||||
|       this->battery_level_ = data[7]; | ||||
|       this->has_battery_level_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_GET_LIGHT_LEVEL: { | ||||
|       VERIFY_MIN_LENGTH(5); | ||||
|       this->light_level_ = 100 * ((float) data[4] / 9); | ||||
|       this->has_light_level_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_GET_POSITION: { | ||||
|       VERIFY_MIN_LENGTH(6); | ||||
|       this->position_ = data[5]; | ||||
|       this->has_position_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_NOTIFY_POSITION: { | ||||
|       VERIFY_MIN_LENGTH(5); | ||||
|       this->position_ = data[4]; | ||||
|       this->has_position_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_SEND_PIN: { | ||||
|       VERIFY_MIN_LENGTH(4); | ||||
|       this->pin_ok_ = data[3] == RESPONSE_ACK; | ||||
|       this->has_pin_response_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_SET_POSITION: { | ||||
|       VERIFY_MIN_LENGTH(4); | ||||
|       this->set_position_ok_ = data[3] == RESPONSE_ACK; | ||||
|       this->has_set_position_response_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case CMD_SET_STATE: { | ||||
|       VERIFY_MIN_LENGTH(4); | ||||
|       this->set_state_ok_ = data[3] == RESPONSE_ACK; | ||||
|       this->has_set_state_response_ = true; | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										78
									
								
								esphome/components/am43/am43_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								esphome/components/am43/am43_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,78 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| static const uint16_t AM43_SERVICE_UUID = 0xFE50; | ||||
| static const uint16_t AM43_CHARACTERISTIC_UUID = 0xFE51; | ||||
| // | ||||
| // Tuya identifiers, only to detect and warn users as they are incompatible. | ||||
| static const uint16_t AM43_TUYA_SERVICE_UUID = 0x1910; | ||||
| static const uint16_t AM43_TUYA_CHARACTERISTIC_UUID = 0x2b11; | ||||
|  | ||||
| struct Am43Packet { | ||||
|   uint8_t length; | ||||
|   uint8_t data[24]; | ||||
| }; | ||||
|  | ||||
| static const uint8_t CMD_GET_BATTERY_LEVEL = 0xA2; | ||||
| static const uint8_t CMD_GET_LIGHT_LEVEL = 0xAA; | ||||
| static const uint8_t CMD_GET_POSITION = 0xA7; | ||||
| static const uint8_t CMD_SEND_PIN = 0x17; | ||||
| static const uint8_t CMD_SET_STATE = 0x0A; | ||||
| static const uint8_t CMD_SET_POSITION = 0x0D; | ||||
| static const uint8_t CMD_NOTIFY_POSITION = 0xA1; | ||||
|  | ||||
| static const uint8_t RESPONSE_ACK = 0x5A; | ||||
| static const uint8_t RESPONSE_NACK = 0xA5; | ||||
|  | ||||
| class Am43Encoder { | ||||
|  public: | ||||
|   Am43Packet *get_battery_level_request(); | ||||
|   Am43Packet *get_light_level_request(); | ||||
|   Am43Packet *get_position_request(); | ||||
|   Am43Packet *get_send_pin_request(uint16_t pin); | ||||
|   Am43Packet *get_open_request(); | ||||
|   Am43Packet *get_close_request(); | ||||
|   Am43Packet *get_stop_request(); | ||||
|   Am43Packet *get_set_position_request(uint8_t position); | ||||
|  | ||||
|  protected: | ||||
|   void checksum_(); | ||||
|   Am43Packet *encode_(uint8_t command, uint8_t *data, uint8_t length); | ||||
|   Am43Packet packet_; | ||||
| }; | ||||
|  | ||||
| class Am43Decoder { | ||||
|  public: | ||||
|   void decode(const uint8_t *data, uint16_t length); | ||||
|   bool has_battery_level() { return this->has_battery_level_; } | ||||
|   bool has_light_level() { return this->has_light_level_; } | ||||
|   bool has_set_position_response() { return this->has_set_position_response_; } | ||||
|   bool has_set_state_response() { return this->has_set_state_response_; } | ||||
|   bool has_position() { return this->has_position_; } | ||||
|   bool has_pin_response() { return this->has_pin_response_; } | ||||
|  | ||||
|   union { | ||||
|     uint8_t position_; | ||||
|     uint8_t battery_level_; | ||||
|     float light_level_; | ||||
|     uint8_t set_position_ok_; | ||||
|     uint8_t set_state_ok_; | ||||
|     uint8_t pin_ok_; | ||||
|   }; | ||||
|  | ||||
|  protected: | ||||
|   bool has_battery_level_; | ||||
|   bool has_light_level_; | ||||
|   bool has_set_position_response_; | ||||
|   bool has_set_state_response_; | ||||
|   bool has_position_; | ||||
|   bool has_pin_response_; | ||||
| }; | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										36
									
								
								esphome/components/am43/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/am43/cover/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import cover, ble_client | ||||
| from esphome.const import CONF_ID, CONF_PIN | ||||
|  | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| AUTO_LOAD = ["am43"] | ||||
|  | ||||
| CONF_INVERT_POSITION = "invert_position" | ||||
|  | ||||
| am43_ns = cg.esphome_ns.namespace("am43") | ||||
| Am43Component = am43_ns.class_( | ||||
|     "Am43Component", cover.Cover, ble_client.BLEClientNode, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cover.COVER_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Am43Component), | ||||
|             cv.Optional(CONF_PIN, default=8888): cv.int_range(min=0, max=0xFFFF), | ||||
|             cv.Optional(CONF_INVERT_POSITION, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_pin(config[CONF_PIN])) | ||||
|     cg.add(var.set_invert_position(config[CONF_INVERT_POSITION])) | ||||
|     yield cg.register_component(var, config) | ||||
|     yield cover.register_cover(var, config) | ||||
|     yield ble_client.register_ble_node(var, config) | ||||
							
								
								
									
										149
									
								
								esphome/components/am43/cover/am43_cover.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								esphome/components/am43/cover/am43_cover.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,149 @@ | ||||
| #include "am43_cover.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| static const char *const TAG = "am43_cover"; | ||||
|  | ||||
| using namespace esphome::cover; | ||||
|  | ||||
| void Am43Component::dump_config() { | ||||
|   LOG_COVER("", "AM43 Cover", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Device Pin: %d", this->pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Invert Position: %d", (int) this->invert_position_); | ||||
| } | ||||
|  | ||||
| void Am43Component::setup() { | ||||
|   this->position = COVER_OPEN; | ||||
|   this->encoder_ = make_unique<Am43Encoder>(); | ||||
|   this->decoder_ = make_unique<Am43Decoder>(); | ||||
|   this->logged_in_ = false; | ||||
| } | ||||
|  | ||||
| void Am43Component::loop() { | ||||
|   if (this->node_state == espbt::ClientState::ESTABLISHED && !this->logged_in_) { | ||||
|     auto packet = this->encoder_->get_send_pin_request(this->pin_); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     ESP_LOGI(TAG, "[%s] Logging into AM43", this->get_name().c_str()); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] Error writing set_pin to device, error = %d", this->get_name().c_str(), status); | ||||
|     else | ||||
|       this->logged_in_ = true; | ||||
|   } | ||||
| } | ||||
|  | ||||
| CoverTraits Am43Component::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_tilt(false); | ||||
|   traits.set_is_assumed_state(false); | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| void Am43Component::control(const CoverCall &call) { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "[%s] Cannot send cover control, not connected", this->get_name().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (call.get_stop()) { | ||||
|     auto packet = this->encoder_->get_stop_request(); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status); | ||||
|   } | ||||
|   if (call.get_position().has_value()) { | ||||
|     auto pos = *call.get_position(); | ||||
|  | ||||
|     if (this->invert_position_) | ||||
|       pos = 1 - pos; | ||||
|     auto packet = this->encoder_->get_set_position_request(100 - (uint8_t)(pos * 100)); | ||||
|     auto status = | ||||
|         esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, packet->length, | ||||
|                                  packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] Error writing set_position command to device, error = %d", this->get_name().c_str(), status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Am43Component::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_DISCONNECT_EVT: { | ||||
|       this->logged_in_ = false; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) { | ||||
|           ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->get_name().c_str()); | ||||
|         } else { | ||||
|           ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->get_name().c_str()); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       this->char_handle_ = chr->handle; | ||||
|  | ||||
|       auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); | ||||
|       if (status) { | ||||
|         ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (param->notify.handle != this->char_handle_) | ||||
|         break; | ||||
|       this->decoder_->decode(param->notify.value, param->notify.value_len); | ||||
|  | ||||
|       if (this->decoder_->has_position()) { | ||||
|         this->position = ((float) this->decoder_->position_ / 100.0); | ||||
|         if (!this->invert_position_) | ||||
|           this->position = 1 - this->position; | ||||
|         if (this->position > 0.97) | ||||
|           this->position = 1.0; | ||||
|         if (this->position < 0.02) | ||||
|           this->position = 0.0; | ||||
|         this->publish_state(); | ||||
|       } | ||||
|  | ||||
|       if (this->decoder_->has_pin_response()) { | ||||
|         if (this->decoder_->pin_ok_) { | ||||
|           ESP_LOGI(TAG, "[%s] AM43 pin accepted.", this->get_name().c_str()); | ||||
|           auto packet = this->encoder_->get_position_request(); | ||||
|           auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                                  packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                                  ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) | ||||
|             ESP_LOGW(TAG, "[%s] Error writing set_position to device, error = %d", this->get_name().c_str(), status); | ||||
|         } else { | ||||
|           ESP_LOGW(TAG, "[%s] AM43 pin rejected!", this->get_name().c_str()); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (this->decoder_->has_set_position_response() && !this->decoder_->set_position_ok_) | ||||
|         ESP_LOGW(TAG, "[%s] Got nack after sending set_position. Bad pin?", this->get_name().c_str()); | ||||
|  | ||||
|       if (this->decoder_->has_set_state_response() && !this->decoder_->set_state_ok_) | ||||
|         ESP_LOGW(TAG, "[%s] Got nack after sending set_state. Bad pin?", this->get_name().c_str()); | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										45
									
								
								esphome/components/am43/cover/am43_cover.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								esphome/components/am43/cover/am43_cover.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/cover/cover.h" | ||||
| #include "esphome/components/am43/am43_base.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am43 { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| class Am43Component : public cover::Cover, public esphome::ble_client::BLEClientNode, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() 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 dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   cover::CoverTraits get_traits() override; | ||||
|   void set_pin(uint16_t pin) { this->pin_ = pin; } | ||||
|   void set_invert_position(bool invert_position) { this->invert_position_ = invert_position; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const cover::CoverCall &call) override; | ||||
|   uint16_t char_handle_; | ||||
|   uint16_t pin_; | ||||
|   bool invert_position_; | ||||
|   std::unique_ptr<Am43Encoder> encoder_; | ||||
|   std::unique_ptr<Am43Decoder> decoder_; | ||||
|   bool logged_in_; | ||||
|  | ||||
|   float position_; | ||||
| }; | ||||
|  | ||||
| }  // namespace am43 | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										46
									
								
								esphome/components/am43/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								esphome/components/am43/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_BATTERY_LEVEL, | ||||
|     ICON_BATTERY, | ||||
|     CONF_ILLUMINANCE, | ||||
|     ICON_BRIGHTNESS_5, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
|  | ||||
| am43_ns = cg.esphome_ns.namespace("am43") | ||||
| Am43 = am43_ns.class_("Am43", ble_client.BLEClientNode, cg.PollingComponent) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Am43), | ||||
|             cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, ICON_BATTERY, 0 | ||||
|             ), | ||||
|             cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema( | ||||
|                 UNIT_PERCENT, ICON_BRIGHTNESS_5, 0 | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("120s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     yield cg.register_component(var, config) | ||||
|     yield ble_client.register_ble_node(var, config) | ||||
|  | ||||
|     if CONF_BATTERY_LEVEL in config: | ||||
|         sens = yield sensor.new_sensor(config[CONF_BATTERY_LEVEL]) | ||||
|         cg.add(var.set_battery(sens)) | ||||
|  | ||||
|     if CONF_ILLUMINANCE in config: | ||||
|         sens = yield sensor.new_sensor(config[CONF_ILLUMINANCE]) | ||||
|         cg.add(var.set_illuminance(sens)) | ||||
| @@ -5,7 +5,7 @@ from esphome.components import display, font | ||||
| import esphome.components.image as espImage | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_FILE, CONF_ID, CONF_TYPE, CONF_RESIZE | ||||
| from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE | ||||
| from esphome.core import CORE, HexInt | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| @@ -15,8 +15,6 @@ MULTI_CONF = True | ||||
|  | ||||
| Animation_ = display.display_ns.class_("Animation") | ||||
|  | ||||
| CONF_RAW_DATA_ID = "raw_data_id" | ||||
|  | ||||
| ANIMATION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_ID): cv.declare_id(Animation_), | ||||
|   | ||||
							
								
								
									
										150
									
								
								esphome/components/anova/anova.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								esphome/components/anova/anova.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| #include "anova.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace anova { | ||||
|  | ||||
| static const char *const TAG = "anova"; | ||||
|  | ||||
| using namespace esphome::climate; | ||||
|  | ||||
| void Anova::dump_config() { LOG_CLIMATE("", "Anova BLE Cooker", this); } | ||||
|  | ||||
| void Anova::setup() { | ||||
|   this->codec_ = make_unique<AnovaCodec>(); | ||||
|   this->current_request_ = 0; | ||||
| } | ||||
|  | ||||
| void Anova::loop() {} | ||||
|  | ||||
| void Anova::control(const ClimateCall &call) { | ||||
|   if (call.get_mode().has_value()) { | ||||
|     ClimateMode mode = *call.get_mode(); | ||||
|     AnovaPacket *pkt; | ||||
|     switch (mode) { | ||||
|       case climate::CLIMATE_MODE_OFF: | ||||
|         pkt = this->codec_->get_stop_request(); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_HEAT: | ||||
|         pkt = this->codec_->get_start_request(); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGW(TAG, "Unsupported mode: %d", mode); | ||||
|         return; | ||||
|     } | ||||
|     auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                            pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|   } | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     auto pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature()); | ||||
|     auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                            pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Anova::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_DISCONNECT_EVT: { | ||||
|       this->current_temperature = NAN; | ||||
|       this->target_temperature = NAN; | ||||
|       this->publish_state(); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str()); | ||||
|         ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->char_handle_ = chr->handle; | ||||
|  | ||||
|       auto status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, chr->handle); | ||||
|       if (status) { | ||||
|         ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|       this->current_request_ = 0; | ||||
|       this->update(); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (param->notify.handle != this->char_handle_) | ||||
|         break; | ||||
|       this->codec_->decode(param->notify.value, param->notify.value_len); | ||||
|       if (this->codec_->has_target_temp()) { | ||||
|         this->target_temperature = this->codec_->target_temp_; | ||||
|       } | ||||
|       if (this->codec_->has_current_temp()) { | ||||
|         this->current_temperature = this->codec_->current_temp_; | ||||
|       } | ||||
|       if (this->codec_->has_running()) { | ||||
|         this->mode = this->codec_->running_ ? climate::CLIMATE_MODE_HEAT : climate::CLIMATE_MODE_OFF; | ||||
|       } | ||||
|       if (this->codec_->has_unit()) { | ||||
|         this->fahrenheit_ = (this->codec_->unit_ == 'f'); | ||||
|         ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius"); | ||||
|         this->current_request_++; | ||||
|       } | ||||
|       this->publish_state(); | ||||
|  | ||||
|       if (this->current_request_ > 1) { | ||||
|         AnovaPacket *pkt = nullptr; | ||||
|         switch (this->current_request_++) { | ||||
|           case 2: | ||||
|             pkt = this->codec_->get_read_target_temp_request(); | ||||
|             break; | ||||
|           case 3: | ||||
|             pkt = this->codec_->get_read_current_temp_request(); | ||||
|             break; | ||||
|           default: | ||||
|             this->current_request_ = 1; | ||||
|             break; | ||||
|         } | ||||
|         if (pkt != nullptr) { | ||||
|           auto status = | ||||
|               esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, pkt->length, | ||||
|                                        pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) | ||||
|             ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), | ||||
|                      status); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Anova::set_unit_of_measurement(const char *unit) { this->fahrenheit_ = !strncmp(unit, "f", 1); } | ||||
|  | ||||
| void Anova::update() { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) | ||||
|     return; | ||||
|  | ||||
|   if (this->current_request_ < 2) { | ||||
|     auto pkt = this->codec_->get_read_device_status_request(); | ||||
|     if (this->current_request_ == 0) | ||||
|       this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c'); | ||||
|     auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_, | ||||
|                                            pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|     if (status) | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     this->current_request_++; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace anova | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										52
									
								
								esphome/components/anova/anova.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								esphome/components/anova/anova.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "anova_base.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace anova { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| static const uint16_t ANOVA_SERVICE_UUID = 0xFFE0; | ||||
| static const uint16_t ANOVA_CHARACTERISTIC_UUID = 0xFFE1; | ||||
|  | ||||
| class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() 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 dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|   climate::ClimateTraits traits() override { | ||||
|     auto traits = climate::ClimateTraits(); | ||||
|     traits.set_supports_current_temperature(true); | ||||
|     traits.set_supports_heat_mode(true); | ||||
|     traits.set_visual_min_temperature(25.0); | ||||
|     traits.set_visual_max_temperature(100.0); | ||||
|     traits.set_visual_temperature_step(0.1); | ||||
|     return traits; | ||||
|   } | ||||
|   void set_unit_of_measurement(const char *); | ||||
|  | ||||
|  protected: | ||||
|   std::unique_ptr<AnovaCodec> codec_; | ||||
|   void control(const climate::ClimateCall &call) override; | ||||
|   uint16_t char_handle_; | ||||
|   uint8_t current_request_; | ||||
|   bool fahrenheit_; | ||||
| }; | ||||
|  | ||||
| }  // namespace anova | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										139
									
								
								esphome/components/anova/anova_base.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								esphome/components/anova/anova_base.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | ||||
| #include "anova_base.h" | ||||
| #include <cstdio> | ||||
| #include <cstring> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace anova { | ||||
|  | ||||
| float ftoc(float f) { return (f - 32.0) * (5.0f / 9.0f); } | ||||
|  | ||||
| float ctof(float c) { return (c * 9.0f / 5.0f) + 32.0; } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::clean_packet_() { | ||||
|   this->packet_.length = strlen((char *) this->packet_.data); | ||||
|   this->packet_.data[this->packet_.length] = '\0'; | ||||
|   ESP_LOGV("anova", "SendPkt: %s\n", this->packet_.data); | ||||
|   return &this->packet_; | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_read_device_status_request() { | ||||
|   this->current_query_ = READ_DEVICE_STATUS; | ||||
|   sprintf((char *) this->packet_.data, "%s", CMD_READ_DEVICE_STATUS); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_read_target_temp_request() { | ||||
|   this->current_query_ = READ_TARGET_TEMPERATURE; | ||||
|   sprintf((char *) this->packet_.data, "%s", CMD_READ_TARGET_TEMP); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_read_current_temp_request() { | ||||
|   this->current_query_ = READ_CURRENT_TEMPERATURE; | ||||
|   sprintf((char *) this->packet_.data, "%s", CMD_READ_CURRENT_TEMP); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_read_unit_request() { | ||||
|   this->current_query_ = READ_UNIT; | ||||
|   sprintf((char *) this->packet_.data, "%s", CMD_READ_UNIT); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_read_data_request() { | ||||
|   this->current_query_ = READ_DATA; | ||||
|   sprintf((char *) this->packet_.data, "%s", CMD_READ_DATA); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_set_target_temp_request(float temperature) { | ||||
|   this->current_query_ = SET_TARGET_TEMPERATURE; | ||||
|   if (this->fahrenheit_) | ||||
|     temperature = ctof(temperature); | ||||
|   sprintf((char *) this->packet_.data, CMD_SET_TARGET_TEMP, temperature); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_set_unit_request(char unit) { | ||||
|   this->current_query_ = SET_UNIT; | ||||
|   sprintf((char *) this->packet_.data, CMD_SET_TEMP_UNIT, unit); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_start_request() { | ||||
|   this->current_query_ = START; | ||||
|   sprintf((char *) this->packet_.data, CMD_START); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| AnovaPacket *AnovaCodec::get_stop_request() { | ||||
|   this->current_query_ = STOP; | ||||
|   sprintf((char *) this->packet_.data, CMD_STOP); | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
|  | ||||
| void AnovaCodec::decode(const uint8_t *data, uint16_t length) { | ||||
|   memset(this->buf_, 0, 32); | ||||
|   strncpy(this->buf_, (char *) data, length); | ||||
|   this->has_target_temp_ = this->has_current_temp_ = this->has_unit_ = this->has_running_ = false; | ||||
|   switch (this->current_query_) { | ||||
|     case READ_DEVICE_STATUS: { | ||||
|       if (!strncmp(this->buf_, "stopped", 7)) { | ||||
|         this->has_running_ = true; | ||||
|         this->running_ = false; | ||||
|       } | ||||
|       if (!strncmp(this->buf_, "running", 7)) { | ||||
|         this->has_running_ = true; | ||||
|         this->running_ = true; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case START: { | ||||
|       if (!strncmp(this->buf_, "start", 5)) { | ||||
|         this->has_running_ = true; | ||||
|         this->running_ = true; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case STOP: { | ||||
|       if (!strncmp(this->buf_, "stop", 4)) { | ||||
|         this->has_running_ = true; | ||||
|         this->running_ = false; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case READ_TARGET_TEMPERATURE: { | ||||
|       this->target_temp_ = strtof(this->buf_, nullptr); | ||||
|       if (this->fahrenheit_) | ||||
|         this->target_temp_ = ftoc(this->target_temp_); | ||||
|       this->has_target_temp_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case SET_TARGET_TEMPERATURE: { | ||||
|       this->target_temp_ = strtof(this->buf_, nullptr); | ||||
|       if (this->fahrenheit_) | ||||
|         this->target_temp_ = ftoc(this->target_temp_); | ||||
|       this->has_target_temp_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case READ_CURRENT_TEMPERATURE: { | ||||
|       this->current_temp_ = strtof(this->buf_, nullptr); | ||||
|       if (this->fahrenheit_) | ||||
|         this->current_temp_ = ftoc(this->current_temp_); | ||||
|       this->has_current_temp_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case SET_UNIT: | ||||
|     case READ_UNIT: { | ||||
|       this->unit_ = this->buf_[0]; | ||||
|       this->fahrenheit_ = this->buf_[0] == 'f'; | ||||
|       this->has_unit_ = true; | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace anova | ||||
| }  // namespace esphome | ||||
							
								
								
									
										80
									
								
								esphome/components/anova/anova_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/anova/anova_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,80 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace anova { | ||||
|  | ||||
| enum CurrentQuery { | ||||
|   NONE, | ||||
|   READ_DEVICE_STATUS, | ||||
|   READ_TARGET_TEMPERATURE, | ||||
|   READ_CURRENT_TEMPERATURE, | ||||
|   READ_DATA, | ||||
|   READ_UNIT, | ||||
|   SET_TARGET_TEMPERATURE, | ||||
|   SET_UNIT, | ||||
|   START, | ||||
|   STOP, | ||||
| }; | ||||
|  | ||||
| struct AnovaPacket { | ||||
|   uint16_t length; | ||||
|   uint8_t data[24]; | ||||
| }; | ||||
|  | ||||
| #define CMD_READ_DEVICE_STATUS "status\r" | ||||
| #define CMD_READ_TARGET_TEMP "read set temp\r" | ||||
| #define CMD_READ_CURRENT_TEMP "read temp\r" | ||||
| #define CMD_READ_UNIT "read unit\r" | ||||
| #define CMD_READ_DATA "read data\r" | ||||
| #define CMD_SET_TARGET_TEMP "set temp %.1f\r" | ||||
| #define CMD_SET_TEMP_UNIT "set unit %c\r" | ||||
|  | ||||
| #define CMD_START "start\r" | ||||
| #define CMD_STOP "stop\r" | ||||
|  | ||||
| class AnovaCodec { | ||||
|  public: | ||||
|   AnovaPacket *get_read_device_status_request(); | ||||
|   AnovaPacket *get_read_target_temp_request(); | ||||
|   AnovaPacket *get_read_current_temp_request(); | ||||
|   AnovaPacket *get_read_data_request(); | ||||
|   AnovaPacket *get_read_unit_request(); | ||||
|  | ||||
|   AnovaPacket *get_set_target_temp_request(float temperature); | ||||
|   AnovaPacket *get_set_unit_request(char unit); | ||||
|  | ||||
|   AnovaPacket *get_start_request(); | ||||
|   AnovaPacket *get_stop_request(); | ||||
|  | ||||
|   void decode(const uint8_t *data, uint16_t length); | ||||
|   bool has_target_temp() { return this->has_target_temp_; } | ||||
|   bool has_current_temp() { return this->has_current_temp_; } | ||||
|   bool has_unit() { return this->has_unit_; } | ||||
|   bool has_running() { return this->has_running_; } | ||||
|  | ||||
|   union { | ||||
|     float target_temp_; | ||||
|     float current_temp_; | ||||
|     char unit_; | ||||
|     bool running_; | ||||
|   }; | ||||
|  | ||||
|  protected: | ||||
|   AnovaPacket *clean_packet_(); | ||||
|   AnovaPacket packet_; | ||||
|  | ||||
|   bool has_target_temp_; | ||||
|   bool has_current_temp_; | ||||
|   bool has_unit_; | ||||
|   bool has_running_; | ||||
|   char buf_[32]; | ||||
|   bool fahrenheit_; | ||||
|  | ||||
|   CurrentQuery current_query_; | ||||
| }; | ||||
|  | ||||
| }  // namespace anova | ||||
| }  // namespace esphome | ||||
							
								
								
									
										36
									
								
								esphome/components/anova/climate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/anova/climate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import climate, ble_client | ||||
| from esphome.const import CONF_ID, CONF_UNIT_OF_MEASUREMENT | ||||
|  | ||||
| UNITS = { | ||||
|     "f": "f", | ||||
|     "c": "c", | ||||
| } | ||||
|  | ||||
| CODEOWNERS = ["@buxtronix"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| anova_ns = cg.esphome_ns.namespace("anova") | ||||
| Anova = anova_ns.class_( | ||||
|     "Anova", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     climate.CLIMATE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Anova), | ||||
|             cv.Required(CONF_UNIT_OF_MEASUREMENT): cv.enum(UNITS), | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await climate.register_climate(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|     cg.add(var.set_unit_of_measurement(config[CONF_UNIT_OF_MEASUREMENT])) | ||||
| @@ -1,5 +1,6 @@ | ||||
| #include "apds9960.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace apds9960 { | ||||
|   | ||||
| @@ -3,7 +3,6 @@ import esphome.config_validation as cv | ||||
| from esphome.components import sensor | ||||
| from esphome.const import ( | ||||
|     CONF_TYPE, | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_PERCENT, | ||||
|     ICON_LIGHTBULB, | ||||
| @@ -21,7 +20,10 @@ TYPES = { | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = sensor.sensor_schema( | ||||
|     UNIT_PERCENT, ICON_LIGHTBULB, 1, DEVICE_CLASS_EMPTY, STATE_CLASS_MEASUREMENT | ||||
|     unit_of_measurement=UNIT_PERCENT, | ||||
|     icon=ICON_LIGHTBULB, | ||||
|     accuracy_decimals=1, | ||||
|     state_class=STATE_CLASS_MEASUREMENT, | ||||
| ).extend( | ||||
|     { | ||||
|         cv.Required(CONF_TYPE): cv.one_of(*TYPES, upper=True), | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import base64 | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| @@ -6,6 +8,7 @@ from esphome.const import ( | ||||
|     CONF_DATA, | ||||
|     CONF_DATA_TEMPLATE, | ||||
|     CONF_ID, | ||||
|     CONF_KEY, | ||||
|     CONF_PASSWORD, | ||||
|     CONF_PORT, | ||||
|     CONF_REBOOT_TIMEOUT, | ||||
| @@ -19,7 +22,7 @@ from esphome.const import ( | ||||
| from esphome.core import coroutine_with_priority | ||||
|  | ||||
| DEPENDENCIES = ["network"] | ||||
| AUTO_LOAD = ["async_tcp"] | ||||
| AUTO_LOAD = ["socket"] | ||||
| CODEOWNERS = ["@OttoWinter"] | ||||
|  | ||||
| api_ns = cg.esphome_ns.namespace("api") | ||||
| @@ -41,6 +44,22 @@ SERVICE_ARG_NATIVE_TYPES = { | ||||
|     "float[]": cg.std_vector.template(float), | ||||
|     "string[]": cg.std_vector.template(cg.std_string), | ||||
| } | ||||
| CONF_ENCRYPTION = "encryption" | ||||
|  | ||||
|  | ||||
| def validate_encryption_key(value): | ||||
|     value = cv.string_strict(value) | ||||
|     try: | ||||
|         decoded = base64.b64decode(value, validate=True) | ||||
|     except ValueError as err: | ||||
|         raise cv.Invalid("Invalid key format, please check it's using base64") from err | ||||
|  | ||||
|     if len(decoded) != 32: | ||||
|         raise cv.Invalid("Encryption key must be base64 and 32 bytes long") | ||||
|  | ||||
|     # Return original data for roundtrip conversion | ||||
|     return value | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
| @@ -63,6 +82,11 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ENCRYPTION): cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_KEY): validate_encryption_key, | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -92,6 +116,15 @@ async def to_code(config): | ||||
|         cg.add(var.register_user_service(trigger)) | ||||
|         await automation.build_automation(trigger, func_args, conf) | ||||
|  | ||||
|     if CONF_ENCRYPTION in config: | ||||
|         conf = config[CONF_ENCRYPTION] | ||||
|         decoded = base64.b64decode(conf[CONF_KEY]) | ||||
|         cg.add(var.set_noise_psk(list(decoded))) | ||||
|         cg.add_define("USE_API_NOISE") | ||||
|         cg.add_library("esphome/noise-c", "0.1.3") | ||||
|     else: | ||||
|         cg.add_define("USE_API_PLAINTEXT") | ||||
|  | ||||
|     cg.add_define("USE_API") | ||||
|     cg.add_global(api_ns.using) | ||||
|  | ||||
|   | ||||
| @@ -38,6 +38,8 @@ service APIConnection { | ||||
|   rpc switch_command (SwitchCommandRequest) returns (void) {} | ||||
|   rpc camera_image (CameraImageRequest) returns (void) {} | ||||
|   rpc climate_command (ClimateCommandRequest) returns (void) {} | ||||
|   rpc number_command (NumberCommandRequest) returns (void) {} | ||||
|   rpc select_command (SelectCommandRequest) returns (void) {} | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -212,6 +214,8 @@ message ListEntitiesBinarySensorResponse { | ||||
|  | ||||
|   string device_class = 5; | ||||
|   bool is_status_binary_sensor = 6; | ||||
|   bool disabled_by_default = 7; | ||||
|   string icon = 8; | ||||
| } | ||||
| message BinarySensorStateResponse { | ||||
|   option (id) = 21; | ||||
| @@ -241,6 +245,8 @@ message ListEntitiesCoverResponse { | ||||
|   bool supports_position = 6; | ||||
|   bool supports_tilt = 7; | ||||
|   string device_class = 8; | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10; | ||||
| } | ||||
|  | ||||
| enum LegacyCoverState { | ||||
| @@ -308,6 +314,8 @@ message ListEntitiesFanResponse { | ||||
|   bool supports_speed = 6; | ||||
|   bool supports_direction = 7; | ||||
|   int32 supported_speed_count = 8; | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10; | ||||
| } | ||||
| enum FanSpeed { | ||||
|   FAN_SPEED_LOW = 0; | ||||
| @@ -351,6 +359,18 @@ message FanCommandRequest { | ||||
| } | ||||
|  | ||||
| // ==================== LIGHT ==================== | ||||
| enum ColorMode { | ||||
|   COLOR_MODE_UNKNOWN = 0; | ||||
|   COLOR_MODE_ON_OFF = 1; | ||||
|   COLOR_MODE_BRIGHTNESS = 2; | ||||
|   COLOR_MODE_WHITE = 7; | ||||
|   COLOR_MODE_COLOR_TEMPERATURE = 11; | ||||
|   COLOR_MODE_COLD_WARM_WHITE = 19; | ||||
|   COLOR_MODE_RGB = 35; | ||||
|   COLOR_MODE_RGB_WHITE = 39; | ||||
|   COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; | ||||
|   COLOR_MODE_RGB_COLD_WARM_WHITE = 51; | ||||
| } | ||||
| message ListEntitiesLightResponse { | ||||
|   option (id) = 15; | ||||
|   option (source) = SOURCE_SERVER; | ||||
| @@ -361,13 +381,17 @@ message ListEntitiesLightResponse { | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   bool supports_brightness = 5; | ||||
|   bool supports_rgb = 6; | ||||
|   bool supports_white_value = 7; | ||||
|   bool supports_color_temperature = 8; | ||||
|   repeated ColorMode supported_color_modes = 12; | ||||
|   // next four supports_* are for legacy clients, newer clients should use color modes | ||||
|   bool legacy_supports_brightness = 5 [deprecated=true]; | ||||
|   bool legacy_supports_rgb = 6 [deprecated=true]; | ||||
|   bool legacy_supports_white_value = 7 [deprecated=true]; | ||||
|   bool legacy_supports_color_temperature = 8 [deprecated=true]; | ||||
|   float min_mireds = 9; | ||||
|   float max_mireds = 10; | ||||
|   repeated string effects = 11; | ||||
|   bool disabled_by_default = 13; | ||||
|   string icon = 14; | ||||
| } | ||||
| message LightStateResponse { | ||||
|   option (id) = 24; | ||||
| @@ -378,11 +402,15 @@ message LightStateResponse { | ||||
|   fixed32 key = 1; | ||||
|   bool state = 2; | ||||
|   float brightness = 3; | ||||
|   ColorMode color_mode = 11; | ||||
|   float color_brightness = 10; | ||||
|   float red = 4; | ||||
|   float green = 5; | ||||
|   float blue = 6; | ||||
|   float white = 7; | ||||
|   float color_temperature = 8; | ||||
|   float cold_white = 12; | ||||
|   float warm_white = 13; | ||||
|   string effect = 9; | ||||
| } | ||||
| message LightCommandRequest { | ||||
| @@ -396,6 +424,10 @@ message LightCommandRequest { | ||||
|   bool state = 3; | ||||
|   bool has_brightness = 4; | ||||
|   float brightness = 5; | ||||
|   bool has_color_mode = 22; | ||||
|   ColorMode color_mode = 23; | ||||
|   bool has_color_brightness = 20; | ||||
|   float color_brightness = 21; | ||||
|   bool has_rgb = 6; | ||||
|   float red = 7; | ||||
|   float green = 8; | ||||
| @@ -404,6 +436,10 @@ message LightCommandRequest { | ||||
|   float white = 11; | ||||
|   bool has_color_temperature = 12; | ||||
|   float color_temperature = 13; | ||||
|   bool has_cold_white = 24; | ||||
|   float cold_white = 25; | ||||
|   bool has_warm_white = 26; | ||||
|   float warm_white = 27; | ||||
|   bool has_transition_length = 14; | ||||
|   uint32 transition_length = 15; | ||||
|   bool has_flash_length = 16; | ||||
| @@ -416,6 +452,13 @@ message LightCommandRequest { | ||||
| enum SensorStateClass { | ||||
|   STATE_CLASS_NONE = 0; | ||||
|   STATE_CLASS_MEASUREMENT = 1; | ||||
|   STATE_CLASS_TOTAL_INCREASING = 2; | ||||
| } | ||||
|  | ||||
| enum SensorLastResetType { | ||||
|   LAST_RESET_NONE = 0; | ||||
|   LAST_RESET_NEVER = 1; | ||||
|   LAST_RESET_AUTO = 2; | ||||
| } | ||||
|  | ||||
| message ListEntitiesSensorResponse { | ||||
| @@ -434,6 +477,9 @@ message ListEntitiesSensorResponse { | ||||
|   bool force_update = 8; | ||||
|   string device_class = 9; | ||||
|   SensorStateClass state_class = 10; | ||||
|   // Last reset type removed in 2021.9.0 | ||||
|   SensorLastResetType legacy_last_reset_type = 11; | ||||
|   bool disabled_by_default = 12; | ||||
| } | ||||
| message SensorStateResponse { | ||||
|   option (id) = 25; | ||||
| @@ -461,6 +507,7 @@ message ListEntitiesSwitchResponse { | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool assumed_state = 6; | ||||
|   bool disabled_by_default = 7; | ||||
| } | ||||
| message SwitchStateResponse { | ||||
|   option (id) = 26; | ||||
| @@ -493,6 +540,7 @@ message ListEntitiesTextSensorResponse { | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
| } | ||||
| message TextSensorStateResponse { | ||||
|   option (id) = 27; | ||||
| @@ -513,9 +561,10 @@ enum LogLevel { | ||||
|   LOG_LEVEL_ERROR = 1; | ||||
|   LOG_LEVEL_WARN = 2; | ||||
|   LOG_LEVEL_INFO = 3; | ||||
|   LOG_LEVEL_DEBUG = 4; | ||||
|   LOG_LEVEL_VERBOSE = 5; | ||||
|   LOG_LEVEL_VERY_VERBOSE = 6; | ||||
|   LOG_LEVEL_CONFIG = 4; | ||||
|   LOG_LEVEL_DEBUG = 5; | ||||
|   LOG_LEVEL_VERBOSE = 6; | ||||
|   LOG_LEVEL_VERY_VERBOSE = 7; | ||||
| } | ||||
| message SubscribeLogsRequest { | ||||
|   option (id) = 28; | ||||
| @@ -530,7 +579,6 @@ message SubscribeLogsResponse { | ||||
|   option (no_delay) = false; | ||||
|  | ||||
|   LogLevel level = 1; | ||||
|   string tag = 2; | ||||
|   string message = 3; | ||||
|   bool send_failed = 4; | ||||
| } | ||||
| @@ -652,6 +700,7 @@ message ListEntitiesCameraResponse { | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|   bool disabled_by_default = 5; | ||||
| } | ||||
|  | ||||
| message CameraImageResponse { | ||||
| @@ -710,13 +759,14 @@ enum ClimateAction { | ||||
|   CLIMATE_ACTION_FAN = 6; | ||||
| } | ||||
| enum ClimatePreset { | ||||
|   CLIMATE_PRESET_ECO = 0; | ||||
|   CLIMATE_PRESET_AWAY = 1; | ||||
|   CLIMATE_PRESET_BOOST = 2; | ||||
|   CLIMATE_PRESET_COMFORT = 3; | ||||
|   CLIMATE_PRESET_HOME = 4; | ||||
|   CLIMATE_PRESET_SLEEP = 5; | ||||
|   CLIMATE_PRESET_ACTIVITY = 6; | ||||
|   CLIMATE_PRESET_NONE = 0; | ||||
|   CLIMATE_PRESET_HOME = 1; | ||||
|   CLIMATE_PRESET_AWAY = 2; | ||||
|   CLIMATE_PRESET_BOOST = 3; | ||||
|   CLIMATE_PRESET_COMFORT = 4; | ||||
|   CLIMATE_PRESET_ECO = 5; | ||||
|   CLIMATE_PRESET_SLEEP = 6; | ||||
|   CLIMATE_PRESET_ACTIVITY = 7; | ||||
| } | ||||
| message ListEntitiesClimateResponse { | ||||
|   option (id) = 46; | ||||
| @@ -734,13 +784,17 @@ message ListEntitiesClimateResponse { | ||||
|   float visual_min_temperature = 8; | ||||
|   float visual_max_temperature = 9; | ||||
|   float visual_temperature_step = 10; | ||||
|   bool supports_away = 11; | ||||
|   // for older peer versions - in new system this | ||||
|   // is if CLIMATE_PRESET_AWAY exists is supported_presets | ||||
|   bool legacy_supports_away = 11; | ||||
|   bool supports_action = 12; | ||||
|   repeated ClimateFanMode supported_fan_modes = 13; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14; | ||||
|   repeated string supported_custom_fan_modes = 15; | ||||
|   repeated ClimatePreset supported_presets = 16; | ||||
|   repeated string supported_custom_presets = 17; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19; | ||||
| } | ||||
| message ClimateStateResponse { | ||||
|   option (id) = 47; | ||||
| @@ -754,7 +808,8 @@ message ClimateStateResponse { | ||||
|   float target_temperature = 4; | ||||
|   float target_temperature_low = 5; | ||||
|   float target_temperature_high = 6; | ||||
|   bool away = 7; | ||||
|   // For older peers, equal to preset == CLIMATE_PRESET_AWAY | ||||
|   bool legacy_away = 7; | ||||
|   ClimateAction action = 8; | ||||
|   ClimateFanMode fan_mode = 9; | ||||
|   ClimateSwingMode swing_mode = 10; | ||||
| @@ -777,8 +832,9 @@ message ClimateCommandRequest { | ||||
|   float target_temperature_low = 7; | ||||
|   bool has_target_temperature_high = 8; | ||||
|   float target_temperature_high = 9; | ||||
|   bool has_away = 10; | ||||
|   bool away = 11; | ||||
|   // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset | ||||
|   bool has_legacy_away = 10; | ||||
|   bool legacy_away = 11; | ||||
|   bool has_fan_mode = 12; | ||||
|   ClimateFanMode fan_mode = 13; | ||||
|   bool has_swing_mode = 14; | ||||
| @@ -790,3 +846,79 @@ message ClimateCommandRequest { | ||||
|   bool has_custom_preset = 20; | ||||
|   string custom_preset = 21; | ||||
| } | ||||
|  | ||||
| // ==================== NUMBER ==================== | ||||
| message ListEntitiesNumberResponse { | ||||
|   option (id) = 49; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_NUMBER"; | ||||
|  | ||||
|   string object_id = 1; | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   float min_value = 6; | ||||
|   float max_value = 7; | ||||
|   float step = 8; | ||||
|   bool disabled_by_default = 9; | ||||
| } | ||||
| message NumberStateResponse { | ||||
|   option (id) = 50; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_NUMBER"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   float state = 2; | ||||
|   // If the number does not have a valid state yet. | ||||
|   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller | ||||
|   bool missing_state = 3; | ||||
| } | ||||
| message NumberCommandRequest { | ||||
|   option (id) = 51; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_NUMBER"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   float state = 2; | ||||
| } | ||||
|  | ||||
| // ==================== SELECT ==================== | ||||
| message ListEntitiesSelectResponse { | ||||
|   option (id) = 52; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SELECT"; | ||||
|  | ||||
|   string object_id = 1; | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   repeated string options = 6; | ||||
|   bool disabled_by_default = 7; | ||||
| } | ||||
| message SelectStateResponse { | ||||
|   option (id) = 53; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SELECT"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   string state = 2; | ||||
|   // If the select does not have a valid state yet. | ||||
|   // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller | ||||
|   bool missing_state = 3; | ||||
| } | ||||
| message SelectCommandRequest { | ||||
|   option (id) = 54; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_SELECT"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|   string state = 2; | ||||
| } | ||||
|   | ||||
| @@ -1,7 +1,10 @@ | ||||
| #include "api_connection.h" | ||||
| #include "esphome/core/entity_base.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/util.h" | ||||
| #include "esphome/components/network/util.h" | ||||
| #include "esphome/core/version.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include <cerrno> | ||||
|  | ||||
| #ifdef USE_DEEP_SLEEP | ||||
| #include "esphome/components/deep_sleep/deep_sleep_component.h" | ||||
| @@ -18,143 +21,144 @@ namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.connection"; | ||||
|  | ||||
| APIConnection::APIConnection(AsyncClient *client, APIServer *parent) | ||||
|     : client_(client), parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { | ||||
|   this->client_->onError([](void *s, AsyncClient *c, int8_t error) { ((APIConnection *) s)->on_error_(error); }, this); | ||||
|   this->client_->onDisconnect([](void *s, AsyncClient *c) { ((APIConnection *) s)->on_disconnect_(); }, this); | ||||
|   this->client_->onTimeout([](void *s, AsyncClient *c, uint32_t time) { ((APIConnection *) s)->on_timeout_(time); }, | ||||
|                            this); | ||||
|   this->client_->onData([](void *s, AsyncClient *c, void *buf, | ||||
|                            size_t len) { ((APIConnection *) s)->on_data_(reinterpret_cast<uint8_t *>(buf), len); }, | ||||
|                         this); | ||||
| APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) | ||||
|     : parent_(parent), initial_state_iterator_(parent, this), list_entities_iterator_(parent, this) { | ||||
|   this->proto_write_buffer_.reserve(64); | ||||
|  | ||||
|   this->send_buffer_.reserve(64); | ||||
|   this->recv_buffer_.reserve(32); | ||||
|   this->client_info_ = this->client_->remoteIP().toString().c_str(); | ||||
| #if defined(USE_API_PLAINTEXT) | ||||
|   helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))}; | ||||
| #elif defined(USE_API_NOISE) | ||||
|   helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())}; | ||||
| #else | ||||
| #error "No frame helper defined" | ||||
| #endif | ||||
| } | ||||
| void APIConnection::start() { | ||||
|   this->last_traffic_ = millis(); | ||||
| } | ||||
| APIConnection::~APIConnection() { delete this->client_; } | ||||
| void APIConnection::on_error_(int8_t error) { this->remove_ = true; } | ||||
| void APIConnection::on_disconnect_() { this->remove_ = true; } | ||||
| void APIConnection::on_timeout_(uint32_t time) { this->on_fatal_error(); } | ||||
| void APIConnection::on_data_(uint8_t *buf, size_t len) { | ||||
|   if (len == 0 || buf == nullptr) | ||||
|  | ||||
|   APIError err = helper_->init(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||
|     return; | ||||
|   this->recv_buffer_.insert(this->recv_buffer_.end(), buf, buf + len); | ||||
| } | ||||
| void APIConnection::parse_recv_buffer_() { | ||||
|   if (this->recv_buffer_.empty() || this->remove_) | ||||
|     return; | ||||
|  | ||||
|   while (!this->recv_buffer_.empty()) { | ||||
|     if (this->recv_buffer_[0] != 0x00) { | ||||
|       ESP_LOGW(TAG, "Invalid preamble from %s", this->client_info_.c_str()); | ||||
|       this->on_fatal_error(); | ||||
|       return; | ||||
|     } | ||||
|     uint32_t i = 1; | ||||
|     const uint32_t size = this->recv_buffer_.size(); | ||||
|     uint32_t consumed; | ||||
|     auto msg_size_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); | ||||
|     if (!msg_size_varint.has_value()) | ||||
|       // not enough data there yet | ||||
|       return; | ||||
|     i += consumed; | ||||
|     uint32_t msg_size = msg_size_varint->as_uint32(); | ||||
|  | ||||
|     auto msg_type_varint = ProtoVarInt::parse(&this->recv_buffer_[i], size - i, &consumed); | ||||
|     if (!msg_type_varint.has_value()) | ||||
|       // not enough data there yet | ||||
|       return; | ||||
|     i += consumed; | ||||
|     uint32_t msg_type = msg_type_varint->as_uint32(); | ||||
|  | ||||
|     if (size - i < msg_size) | ||||
|       // message body not fully received | ||||
|       return; | ||||
|  | ||||
|     uint8_t *msg = &this->recv_buffer_[i]; | ||||
|     this->read_message(msg_size, msg_type, msg); | ||||
|     if (this->remove_) | ||||
|       return; | ||||
|     // pop front | ||||
|     uint32_t total = i + msg_size; | ||||
|     this->recv_buffer_.erase(this->recv_buffer_.begin(), this->recv_buffer_.begin() + total); | ||||
|     this->last_traffic_ = millis(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void APIConnection::disconnect_client() { | ||||
|   this->client_->close(); | ||||
|   this->remove_ = true; | ||||
|   client_info_ = helper_->getpeername(); | ||||
|   helper_->set_log_info(client_info_); | ||||
| } | ||||
|  | ||||
| void APIConnection::loop() { | ||||
|   if (this->remove_) | ||||
|     return; | ||||
|  | ||||
|   if (this->next_close_) { | ||||
|     this->disconnect_client(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (!network_is_connected()) { | ||||
|   if (!network::is_connected()) { | ||||
|     // when network is disconnected force disconnect immediately | ||||
|     // don't wait for timeout | ||||
|     this->on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Network unavailable, disconnecting", client_info_.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (this->client_->disconnected()) { | ||||
|     // failsafe for disconnect logic | ||||
|     this->on_disconnect_(); | ||||
|   if (this->next_close_) { | ||||
|     // requested a disconnect | ||||
|     this->helper_->close(); | ||||
|     this->remove_ = true; | ||||
|     return; | ||||
|   } | ||||
|   this->parse_recv_buffer_(); | ||||
|  | ||||
|   APIError err = helper_->loop(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||
|     return; | ||||
|   } | ||||
|   ReadPacketBuffer buffer; | ||||
|   err = helper_->read_packet(&buffer); | ||||
|   if (err == APIError::WOULD_BLOCK) { | ||||
|     // pass | ||||
|   } else if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||
|     } | ||||
|     return; | ||||
|   } else { | ||||
|     this->last_traffic_ = millis(); | ||||
|     // read a packet | ||||
|     this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); | ||||
|     if (this->remove_) | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   this->list_entities_iterator_.advance(); | ||||
|   this->initial_state_iterator_.advance(); | ||||
|  | ||||
|   const uint32_t keepalive = 60000; | ||||
|   const uint32_t now = millis(); | ||||
|   if (this->sent_ping_) { | ||||
|     // Disconnect if not responded within 2.5*keepalive | ||||
|     if (millis() - this->last_traffic_ > (keepalive * 5) / 2) { | ||||
|       ESP_LOGW(TAG, "'%s' didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); | ||||
|       this->disconnect_client(); | ||||
|     if (now - this->last_traffic_ > (keepalive * 5) / 2) { | ||||
|       on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_info_.c_str()); | ||||
|     } | ||||
|   } else if (millis() - this->last_traffic_ > keepalive) { | ||||
|   } else if (now - this->last_traffic_ > keepalive) { | ||||
|     this->sent_ping_ = true; | ||||
|     this->send_ping_request(PingRequest()); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   if (this->image_reader_.available()) { | ||||
|     uint32_t space = this->client_->space(); | ||||
|     // reserve 15 bytes for metadata, and at least 64 bytes of data | ||||
|     if (space >= 15 + 64) { | ||||
|       uint32_t to_send = std::min(space - 15, this->image_reader_.available()); | ||||
|       auto buffer = this->create_buffer(); | ||||
|       // fixed32 key = 1; | ||||
|       buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); | ||||
|       // bytes data = 2; | ||||
|       buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); | ||||
|       // bool done = 3; | ||||
|       bool done = this->image_reader_.available() == to_send; | ||||
|       buffer.encode_bool(3, done); | ||||
|       bool success = this->send_buffer(buffer, 44); | ||||
|   if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) { | ||||
|     uint32_t to_send = std::min((size_t) 1024, this->image_reader_.available()); | ||||
|     auto buffer = this->create_buffer(); | ||||
|     // fixed32 key = 1; | ||||
|     buffer.encode_fixed32(1, esp32_camera::global_esp32_camera->get_object_id_hash()); | ||||
|     // bytes data = 2; | ||||
|     buffer.encode_bytes(2, this->image_reader_.peek_data_buffer(), to_send); | ||||
|     // bool done = 3; | ||||
|     bool done = this->image_reader_.available() == to_send; | ||||
|     buffer.encode_bool(3, done); | ||||
|     bool success = this->send_buffer(buffer, 44); | ||||
|  | ||||
|       if (success) { | ||||
|         this->image_reader_.consume_data(to_send); | ||||
|       } | ||||
|       if (success && done) { | ||||
|         this->image_reader_.return_image(); | ||||
|       } | ||||
|     if (success) { | ||||
|       this->image_reader_.consume_data(to_send); | ||||
|     } | ||||
|     if (success && done) { | ||||
|       this->image_reader_.return_image(); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   if (state_subs_at_ != -1) { | ||||
|     const auto &subs = this->parent_->get_state_subs(); | ||||
|     if (state_subs_at_ >= subs.size()) { | ||||
|       state_subs_at_ = -1; | ||||
|     } else { | ||||
|       auto &it = subs[state_subs_at_]; | ||||
|       SubscribeHomeAssistantStateResponse resp; | ||||
|       resp.entity_id = it.entity_id; | ||||
|       resp.attribute = it.attribute.value(); | ||||
|       if (this->send_subscribe_home_assistant_state_response(resp)) { | ||||
|         state_subs_at_++; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::string get_default_unique_id(const std::string &component_type, Nameable *nameable) { | ||||
|   return App.get_name() + component_type + nameable->get_object_id(); | ||||
| std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) { | ||||
|   return App.get_name() + component_type + entity->get_object_id(); | ||||
| } | ||||
|  | ||||
| DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { | ||||
|   // remote initiated disconnect_client | ||||
|   // don't close yet, we still need to send the disconnect response | ||||
|   // close will happen on next loop | ||||
|   ESP_LOGD(TAG, "%s requested disconnected", client_info_.c_str()); | ||||
|   this->next_close_ = true; | ||||
|   DisconnectResponse resp; | ||||
|   return resp; | ||||
| } | ||||
| void APIConnection::on_disconnect_response(const DisconnectResponse &value) { | ||||
|   // pass | ||||
| } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| @@ -176,6 +180,8 @@ bool APIConnection::send_binary_sensor_info(binary_sensor::BinarySensor *binary_ | ||||
|   msg.unique_id = get_default_unique_id("binary_sensor", binary_sensor); | ||||
|   msg.device_class = binary_sensor->get_device_class(); | ||||
|   msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor(); | ||||
|   msg.disabled_by_default = binary_sensor->is_disabled_by_default(); | ||||
|   msg.icon = binary_sensor->get_icon(); | ||||
|   return this->send_list_entities_binary_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
| @@ -207,6 +213,8 @@ bool APIConnection::send_cover_info(cover::Cover *cover) { | ||||
|   msg.supports_position = traits.get_supports_position(); | ||||
|   msg.supports_tilt = traits.get_supports_tilt(); | ||||
|   msg.device_class = cover->get_device_class(); | ||||
|   msg.disabled_by_default = cover->is_disabled_by_default(); | ||||
|   msg.icon = cover->get_icon(); | ||||
|   return this->send_list_entities_cover_response(msg); | ||||
| } | ||||
| void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
| @@ -239,6 +247,9 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
| // Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
| bool APIConnection::send_fan_state(fan::FanState *fan) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
| @@ -268,6 +279,8 @@ bool APIConnection::send_fan_info(fan::FanState *fan) { | ||||
|   msg.supports_speed = traits.supports_speed(); | ||||
|   msg.supports_direction = traits.supports_direction(); | ||||
|   msg.supported_speed_count = traits.supported_speed_count(); | ||||
|   msg.disabled_by_default = fan->is_disabled_by_default(); | ||||
|   msg.icon = fan->get_icon(); | ||||
|   return this->send_list_entities_fan_response(msg); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
| @@ -292,6 +305,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||
|   call.perform(); | ||||
| } | ||||
| #pragma GCC diagnostic pop | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
| @@ -301,21 +315,21 @@ bool APIConnection::send_light_state(light::LightState *light) { | ||||
|  | ||||
|   auto traits = light->get_traits(); | ||||
|   auto values = light->remote_values; | ||||
|   auto color_mode = values.get_color_mode(); | ||||
|   LightStateResponse resp{}; | ||||
|  | ||||
|   resp.key = light->get_object_id_hash(); | ||||
|   resp.state = values.is_on(); | ||||
|   if (traits.get_supports_brightness()) | ||||
|     resp.brightness = values.get_brightness(); | ||||
|   if (traits.get_supports_rgb()) { | ||||
|     resp.red = values.get_red(); | ||||
|     resp.green = values.get_green(); | ||||
|     resp.blue = values.get_blue(); | ||||
|   } | ||||
|   if (traits.get_supports_rgb_white_value()) | ||||
|     resp.white = values.get_white(); | ||||
|   if (traits.get_supports_color_temperature()) | ||||
|     resp.color_temperature = values.get_color_temperature(); | ||||
|   resp.color_mode = static_cast<enums::ColorMode>(color_mode); | ||||
|   resp.brightness = values.get_brightness(); | ||||
|   resp.color_brightness = values.get_color_brightness(); | ||||
|   resp.red = values.get_red(); | ||||
|   resp.green = values.get_green(); | ||||
|   resp.blue = values.get_blue(); | ||||
|   resp.white = values.get_white(); | ||||
|   resp.color_temperature = values.get_color_temperature(); | ||||
|   resp.cold_white = values.get_cold_white(); | ||||
|   resp.warm_white = values.get_warm_white(); | ||||
|   if (light->supports_effects()) | ||||
|     resp.effect = light->get_effect_name(); | ||||
|   return this->send_light_state_response(resp); | ||||
| @@ -327,11 +341,22 @@ bool APIConnection::send_light_info(light::LightState *light) { | ||||
|   msg.object_id = light->get_object_id(); | ||||
|   msg.name = light->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("light", light); | ||||
|   msg.supports_brightness = traits.get_supports_brightness(); | ||||
|   msg.supports_rgb = traits.get_supports_rgb(); | ||||
|   msg.supports_white_value = traits.get_supports_rgb_white_value(); | ||||
|   msg.supports_color_temperature = traits.get_supports_color_temperature(); | ||||
|   if (msg.supports_color_temperature) { | ||||
|  | ||||
|   msg.disabled_by_default = light->is_disabled_by_default(); | ||||
|   msg.icon = light->get_icon(); | ||||
|  | ||||
|   for (auto mode : traits.get_supported_color_modes()) | ||||
|     msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode)); | ||||
|  | ||||
|   msg.legacy_supports_brightness = traits.supports_color_capability(light::ColorCapability::BRIGHTNESS); | ||||
|   msg.legacy_supports_rgb = traits.supports_color_capability(light::ColorCapability::RGB); | ||||
|   msg.legacy_supports_white_value = | ||||
|       msg.legacy_supports_rgb && (traits.supports_color_capability(light::ColorCapability::WHITE) || | ||||
|                                   traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)); | ||||
|   msg.legacy_supports_color_temperature = traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) || | ||||
|                                           traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE); | ||||
|  | ||||
|   if (msg.legacy_supports_color_temperature) { | ||||
|     msg.min_mireds = traits.get_min_mireds(); | ||||
|     msg.max_mireds = traits.get_max_mireds(); | ||||
|   } | ||||
| @@ -352,6 +377,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { | ||||
|     call.set_state(msg.state); | ||||
|   if (msg.has_brightness) | ||||
|     call.set_brightness(msg.brightness); | ||||
|   if (msg.has_color_mode) | ||||
|     call.set_color_mode(static_cast<light::ColorMode>(msg.color_mode)); | ||||
|   if (msg.has_color_brightness) | ||||
|     call.set_color_brightness(msg.color_brightness); | ||||
|   if (msg.has_rgb) { | ||||
|     call.set_red(msg.red); | ||||
|     call.set_green(msg.green); | ||||
| @@ -361,6 +390,10 @@ void APIConnection::light_command(const LightCommandRequest &msg) { | ||||
|     call.set_white(msg.white); | ||||
|   if (msg.has_color_temperature) | ||||
|     call.set_color_temperature(msg.color_temperature); | ||||
|   if (msg.has_cold_white) | ||||
|     call.set_cold_white(msg.cold_white); | ||||
|   if (msg.has_warm_white) | ||||
|     call.set_warm_white(msg.warm_white); | ||||
|   if (msg.has_transition_length) | ||||
|     call.set_transition_length(msg.transition_length); | ||||
|   if (msg.has_flash_length) | ||||
| @@ -395,7 +428,8 @@ bool APIConnection::send_sensor_info(sensor::Sensor *sensor) { | ||||
|   msg.accuracy_decimals = sensor->get_accuracy_decimals(); | ||||
|   msg.force_update = sensor->get_force_update(); | ||||
|   msg.device_class = sensor->get_device_class(); | ||||
|   msg.state_class = static_cast<enums::SensorStateClass>(sensor->state_class); | ||||
|   msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class()); | ||||
|   msg.disabled_by_default = sensor->is_disabled_by_default(); | ||||
|  | ||||
|   return this->send_list_entities_sensor_response(msg); | ||||
| } | ||||
| @@ -419,6 +453,7 @@ bool APIConnection::send_switch_info(switch_::Switch *a_switch) { | ||||
|   msg.unique_id = get_default_unique_id("switch", a_switch); | ||||
|   msg.icon = a_switch->get_icon(); | ||||
|   msg.assumed_state = a_switch->assumed_state(); | ||||
|   msg.disabled_by_default = a_switch->is_disabled_by_default(); | ||||
|   return this->send_list_entities_switch_response(msg); | ||||
| } | ||||
| void APIConnection::switch_command(const SwitchCommandRequest &msg) { | ||||
| @@ -453,6 +488,7 @@ bool APIConnection::send_text_sensor_info(text_sensor::TextSensor *text_sensor) | ||||
|   if (msg.unique_id.empty()) | ||||
|     msg.unique_id = get_default_unique_id("text_sensor", text_sensor); | ||||
|   msg.icon = text_sensor->get_icon(); | ||||
|   msg.disabled_by_default = text_sensor->is_disabled_by_default(); | ||||
|   return this->send_list_entities_text_sensor_response(msg); | ||||
| } | ||||
| #endif | ||||
| @@ -475,14 +511,14 @@ bool APIConnection::send_climate_state(climate::Climate *climate) { | ||||
|   } else { | ||||
|     resp.target_temperature = climate->target_temperature; | ||||
|   } | ||||
|   if (traits.get_supports_away()) | ||||
|     resp.away = climate->away; | ||||
|   if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) | ||||
|     resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) | ||||
|     resp.custom_fan_mode = climate->custom_fan_mode.value(); | ||||
|   if (traits.get_supports_presets() && climate->preset.has_value()) | ||||
|   if (traits.get_supports_presets() && climate->preset.has_value()) { | ||||
|     resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); | ||||
|     resp.legacy_away = resp.preset == enums::CLIMATE_PRESET_AWAY; | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) | ||||
|     resp.custom_preset = climate->custom_preset.value(); | ||||
|   if (traits.get_supports_swing_modes()) | ||||
| @@ -496,42 +532,32 @@ bool APIConnection::send_climate_info(climate::Climate *climate) { | ||||
|   msg.object_id = climate->get_object_id(); | ||||
|   msg.name = climate->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("climate", climate); | ||||
|  | ||||
|   msg.disabled_by_default = climate->is_disabled_by_default(); | ||||
|   msg.icon = climate->get_icon(); | ||||
|  | ||||
|   msg.supports_current_temperature = traits.get_supports_current_temperature(); | ||||
|   msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature(); | ||||
|   for (auto mode : | ||||
|        {climate::CLIMATE_MODE_AUTO, climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_COOL, climate::CLIMATE_MODE_HEAT, | ||||
|         climate::CLIMATE_MODE_DRY, climate::CLIMATE_MODE_FAN_ONLY, climate::CLIMATE_MODE_HEAT_COOL}) { | ||||
|     if (traits.supports_mode(mode)) | ||||
|       msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); | ||||
|   } | ||||
|  | ||||
|   for (auto mode : traits.get_supported_modes()) | ||||
|     msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode)); | ||||
|  | ||||
|   msg.visual_min_temperature = traits.get_visual_min_temperature(); | ||||
|   msg.visual_max_temperature = traits.get_visual_max_temperature(); | ||||
|   msg.visual_temperature_step = traits.get_visual_temperature_step(); | ||||
|   msg.supports_away = traits.get_supports_away(); | ||||
|   msg.legacy_supports_away = traits.supports_preset(climate::CLIMATE_PRESET_AWAY); | ||||
|   msg.supports_action = traits.get_supports_action(); | ||||
|   for (auto fan_mode : {climate::CLIMATE_FAN_ON, climate::CLIMATE_FAN_OFF, climate::CLIMATE_FAN_AUTO, | ||||
|                         climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH, | ||||
|                         climate::CLIMATE_FAN_MIDDLE, climate::CLIMATE_FAN_FOCUS, climate::CLIMATE_FAN_DIFFUSE}) { | ||||
|     if (traits.supports_fan_mode(fan_mode)) | ||||
|       msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); | ||||
|   } | ||||
|   for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) { | ||||
|  | ||||
|   for (auto fan_mode : traits.get_supported_fan_modes()) | ||||
|     msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode)); | ||||
|   for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) | ||||
|     msg.supported_custom_fan_modes.push_back(custom_fan_mode); | ||||
|   } | ||||
|   for (auto preset : {climate::CLIMATE_PRESET_ECO, climate::CLIMATE_PRESET_AWAY, climate::CLIMATE_PRESET_BOOST, | ||||
|                       climate::CLIMATE_PRESET_COMFORT, climate::CLIMATE_PRESET_HOME, climate::CLIMATE_PRESET_SLEEP, | ||||
|                       climate::CLIMATE_PRESET_ACTIVITY}) { | ||||
|     if (traits.supports_preset(preset)) | ||||
|       msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset)); | ||||
|   } | ||||
|   for (auto const &custom_preset : traits.get_supported_custom_presets()) { | ||||
|   for (auto preset : traits.get_supported_presets()) | ||||
|     msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset)); | ||||
|   for (auto const &custom_preset : traits.get_supported_custom_presets()) | ||||
|     msg.supported_custom_presets.push_back(custom_preset); | ||||
|   } | ||||
|   for (auto swing_mode : {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH, climate::CLIMATE_SWING_VERTICAL, | ||||
|                           climate::CLIMATE_SWING_HORIZONTAL}) { | ||||
|     if (traits.supports_swing_mode(swing_mode)) | ||||
|       msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode)); | ||||
|   } | ||||
|   for (auto swing_mode : traits.get_supported_swing_modes()) | ||||
|     msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode)); | ||||
|   return this->send_list_entities_climate_response(msg); | ||||
| } | ||||
| void APIConnection::climate_command(const ClimateCommandRequest &msg) { | ||||
| @@ -548,8 +574,8 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { | ||||
|     call.set_target_temperature_low(msg.target_temperature_low); | ||||
|   if (msg.has_target_temperature_high) | ||||
|     call.set_target_temperature_high(msg.target_temperature_high); | ||||
|   if (msg.has_away) | ||||
|     call.set_away(msg.away); | ||||
|   if (msg.has_legacy_away) | ||||
|     call.set_preset(msg.legacy_away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME); | ||||
|   if (msg.has_fan_mode) | ||||
|     call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode)); | ||||
|   if (msg.has_custom_fan_mode) | ||||
| @@ -564,13 +590,86 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_NUMBER | ||||
| bool APIConnection::send_number_state(number::Number *number, float state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   NumberStateResponse resp{}; | ||||
|   resp.key = number->get_object_id_hash(); | ||||
|   resp.state = state; | ||||
|   resp.missing_state = !number->has_state(); | ||||
|   return this->send_number_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_number_info(number::Number *number) { | ||||
|   ListEntitiesNumberResponse msg; | ||||
|   msg.key = number->get_object_id_hash(); | ||||
|   msg.object_id = number->get_object_id(); | ||||
|   msg.name = number->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("number", number); | ||||
|   msg.icon = number->get_icon(); | ||||
|   msg.disabled_by_default = number->is_disabled_by_default(); | ||||
|  | ||||
|   msg.min_value = number->traits.get_min_value(); | ||||
|   msg.max_value = number->traits.get_max_value(); | ||||
|   msg.step = number->traits.get_step(); | ||||
|  | ||||
|   return this->send_list_entities_number_response(msg); | ||||
| } | ||||
| void APIConnection::number_command(const NumberCommandRequest &msg) { | ||||
|   number::Number *number = App.get_number_by_key(msg.key); | ||||
|   if (number == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto call = number->make_call(); | ||||
|   call.set_value(msg.state); | ||||
|   call.perform(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_SELECT | ||||
| bool APIConnection::send_select_state(select::Select *select, std::string state) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   SelectStateResponse resp{}; | ||||
|   resp.key = select->get_object_id_hash(); | ||||
|   resp.state = std::move(state); | ||||
|   resp.missing_state = !select->has_state(); | ||||
|   return this->send_select_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_select_info(select::Select *select) { | ||||
|   ListEntitiesSelectResponse msg; | ||||
|   msg.key = select->get_object_id_hash(); | ||||
|   msg.object_id = select->get_object_id(); | ||||
|   msg.name = select->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("select", select); | ||||
|   msg.icon = select->get_icon(); | ||||
|   msg.disabled_by_default = select->is_disabled_by_default(); | ||||
|  | ||||
|   for (const auto &option : select->traits.get_options()) | ||||
|     msg.options.push_back(option); | ||||
|  | ||||
|   return this->send_list_entities_select_response(msg); | ||||
| } | ||||
| void APIConnection::select_command(const SelectCommandRequest &msg) { | ||||
|   select::Select *select = App.get_select_by_key(msg.key); | ||||
|   if (select == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto call = select->make_call(); | ||||
|   call.set_option(msg.state); | ||||
|   call.perform(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
|   if (!this->state_subscription_) | ||||
|     return; | ||||
|   if (this->image_reader_.available()) | ||||
|     return; | ||||
|   this->image_reader_.set_image(image); | ||||
|   this->image_reader_.set_image(std::move(image)); | ||||
| } | ||||
| bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   ListEntitiesCameraResponse msg; | ||||
| @@ -578,6 +677,7 @@ bool APIConnection::send_camera_info(esp32_camera::ESP32Camera *camera) { | ||||
|   msg.object_id = camera->get_object_id(); | ||||
|   msg.name = camera->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("camera", camera); | ||||
|   msg.disabled_by_default = camera->is_disabled_by_default(); | ||||
|   return this->send_list_entities_camera_response(msg); | ||||
| } | ||||
| void APIConnection::camera_image(const CameraImageRequest &msg) { | ||||
| @@ -606,30 +706,20 @@ bool APIConnection::send_log_message(int level, const char *tag, const char *lin | ||||
|   auto buffer = this->create_buffer(); | ||||
|   // LogLevel level = 1; | ||||
|   buffer.encode_uint32(1, static_cast<uint32_t>(level)); | ||||
|   // string tag = 2; | ||||
|   // buffer.encode_string(2, tag, strlen(tag)); | ||||
|   // string message = 3; | ||||
|   buffer.encode_string(3, line, strlen(line)); | ||||
|   // SubscribeLogsResponse - 29 | ||||
|   bool success = this->send_buffer(buffer, 29); | ||||
|   if (!success) { | ||||
|     buffer = this->create_buffer(); | ||||
|     // bool send_failed = 4; | ||||
|     buffer.encode_bool(4, true); | ||||
|     return this->send_buffer(buffer, 29); | ||||
|   } else { | ||||
|     return true; | ||||
|   } | ||||
|   return this->send_buffer(buffer, 29); | ||||
| } | ||||
|  | ||||
| HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   this->client_info_ = msg.client_info + " (" + this->client_->remoteIP().toString().c_str(); | ||||
|   this->client_info_ += ")"; | ||||
|   this->client_info_ = msg.client_info + " (" + this->helper_->getpeername() + ")"; | ||||
|   this->helper_->set_log_info(client_info_); | ||||
|   ESP_LOGV(TAG, "Hello from client: '%s'", this->client_info_.c_str()); | ||||
|  | ||||
|   HelloResponse resp; | ||||
|   resp.api_version_major = 1; | ||||
|   resp.api_version_minor = 4; | ||||
|   resp.api_version_minor = 6; | ||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||
|   this->connection_state_ = ConnectionState::CONNECTED; | ||||
|   return resp; | ||||
| @@ -641,7 +731,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | ||||
|   // bool invalid_password = 1; | ||||
|   resp.invalid_password = !correct; | ||||
|   if (correct) { | ||||
|     ESP_LOGD(TAG, "Client '%s' connected successfully!", this->client_info_.c_str()); | ||||
|     ESP_LOGD(TAG, "%s: Connected successfully", this->client_info_.c_str()); | ||||
|     this->connection_state_ = ConnectionState::AUTHENTICATED; | ||||
|  | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
| @@ -659,9 +749,7 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
|   resp.mac_address = get_mac_address_pretty(); | ||||
|   resp.esphome_version = ESPHOME_VERSION; | ||||
|   resp.compilation_time = App.get_compilation_time(); | ||||
| #ifdef ARDUINO_BOARD | ||||
|   resp.model = ARDUINO_BOARD; | ||||
| #endif | ||||
|   resp.model = ESPHOME_BOARD; | ||||
| #ifdef USE_DEEP_SLEEP | ||||
|   resp.has_deep_sleep = deep_sleep::global_has_deep_sleep; | ||||
| #endif | ||||
| @@ -689,30 +777,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) { | ||||
|   } | ||||
| } | ||||
| void APIConnection::subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) { | ||||
|   for (auto &it : this->parent_->get_state_subs()) { | ||||
|     SubscribeHomeAssistantStateResponse resp; | ||||
|     resp.entity_id = it.entity_id; | ||||
|     resp.attribute = it.attribute.value(); | ||||
|     if (!this->send_subscribe_home_assistant_state_response(resp)) { | ||||
|       this->on_fatal_error(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   state_subs_at_ = 0; | ||||
| } | ||||
| bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) { | ||||
|   if (this->remove_) | ||||
|     return false; | ||||
|  | ||||
|   std::vector<uint8_t> header; | ||||
|   header.push_back(0x00); | ||||
|   ProtoVarInt(buffer.get_buffer()->size()).encode(header); | ||||
|   ProtoVarInt(message_type).encode(header); | ||||
|  | ||||
|   size_t needed_space = buffer.get_buffer()->size() + header.size(); | ||||
|  | ||||
|   if (needed_space > this->client_->space()) { | ||||
|   if (!this->helper_->can_write_without_blocking()) { | ||||
|     delay(0); | ||||
|     if (needed_space > this->client_->space()) { | ||||
|     APIError err = helper_->loop(); | ||||
|     if (err != APIError::OK) { | ||||
|       on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||
|       return false; | ||||
|     } | ||||
|     if (!this->helper_->can_write_without_blocking()) { | ||||
|       // SubscribeLogsResponse | ||||
|       if (message_type != 29) { | ||||
|         ESP_LOGV(TAG, "Cannot send message because of TCP buffer space"); | ||||
| @@ -722,24 +800,31 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   this->client_->add(reinterpret_cast<char *>(header.data()), header.size(), | ||||
|                      ASYNC_WRITE_FLAG_COPY | ASYNC_WRITE_FLAG_MORE); | ||||
|   this->client_->add(reinterpret_cast<char *>(buffer.get_buffer()->data()), buffer.get_buffer()->size(), | ||||
|                      ASYNC_WRITE_FLAG_COPY); | ||||
|   bool ret = this->client_->send(); | ||||
|   return ret; | ||||
|   APIError err = this->helper_->write_packet(message_type, buffer.get_buffer()->data(), buffer.get_buffer()->size()); | ||||
|   if (err == APIError::WOULD_BLOCK) | ||||
|     return false; | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset", client_info_.c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", client_info_.c_str(), api_error_to_str(err), errno); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|   this->last_traffic_ = millis(); | ||||
|   return true; | ||||
| } | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   ESP_LOGD(TAG, "'%s' tried to access without authentication.", this->client_info_.c_str()); | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s: tried to access without authentication.", this->client_info_.c_str()); | ||||
| } | ||||
| void APIConnection::on_no_setup_connection() { | ||||
|   ESP_LOGD(TAG, "'%s' tried to access without full connection.", this->client_info_.c_str()); | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s: tried to access without full connection.", this->client_info_.c_str()); | ||||
| } | ||||
| void APIConnection::on_fatal_error() { | ||||
|   ESP_LOGV(TAG, "Error: Disconnecting %s", this->client_info_.c_str()); | ||||
|   this->client_->close(); | ||||
|   this->helper_->close(); | ||||
|   this->remove_ = true; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -5,16 +5,17 @@ | ||||
| #include "api_pb2.h" | ||||
| #include "api_pb2_service.h" | ||||
| #include "api_server.h" | ||||
| #include "api_frame_helper.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| class APIConnection : public APIServerConnection { | ||||
|  public: | ||||
|   APIConnection(AsyncClient *client, APIServer *parent); | ||||
|   virtual ~APIConnection(); | ||||
|   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); | ||||
|   virtual ~APIConnection() = default; | ||||
|  | ||||
|   void disconnect_client(); | ||||
|   void start(); | ||||
|   void loop(); | ||||
|  | ||||
|   bool send_list_info_done() { | ||||
| @@ -62,6 +63,16 @@ class APIConnection : public APIServerConnection { | ||||
|   bool send_climate_state(climate::Climate *climate); | ||||
|   bool send_climate_info(climate::Climate *climate); | ||||
|   void climate_command(const ClimateCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
|   bool send_number_state(number::Number *number, float state); | ||||
|   bool send_number_info(number::Number *number); | ||||
|   void number_command(const NumberCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   bool send_select_state(select::Select *select, std::string state); | ||||
|   bool send_select_info(select::Select *select); | ||||
|   void select_command(const SelectCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool send_log_message(int level, const char *tag, const char *line); | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
| @@ -76,10 +87,7 @@ class APIConnection : public APIServerConnection { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   void on_disconnect_response(const DisconnectResponse &value) override { | ||||
|     // we initiated disconnect_client | ||||
|     this->next_close_ = true; | ||||
|   } | ||||
|   void on_disconnect_response(const DisconnectResponse &value) override; | ||||
|   void on_ping_response(const PingResponse &value) override { | ||||
|     // we initiated ping | ||||
|     this->sent_ping_ = false; | ||||
| @@ -90,12 +98,7 @@ class APIConnection : public APIServerConnection { | ||||
| #endif | ||||
|   HelloResponse hello(const HelloRequest &msg) override; | ||||
|   ConnectResponse connect(const ConnectRequest &msg) override; | ||||
|   DisconnectResponse disconnect(const DisconnectRequest &msg) override { | ||||
|     // remote initiated disconnect_client | ||||
|     this->next_close_ = true; | ||||
|     DisconnectResponse resp; | ||||
|     return resp; | ||||
|   } | ||||
|   DisconnectResponse disconnect(const DisconnectRequest &msg) override; | ||||
|   PingResponse ping(const PingRequest &msg) override { return {}; } | ||||
|   DeviceInfoResponse device_info(const DeviceInfoRequest &msg) override; | ||||
|   void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); } | ||||
| @@ -125,19 +128,16 @@ class APIConnection : public APIServerConnection { | ||||
|   void on_unauthenticated_access() override; | ||||
|   void on_no_setup_connection() override; | ||||
|   ProtoWriteBuffer create_buffer() override { | ||||
|     this->send_buffer_.clear(); | ||||
|     return {&this->send_buffer_}; | ||||
|     // FIXME: ensure no recursive writes can happen | ||||
|     this->proto_write_buffer_.clear(); | ||||
|     return {&this->proto_write_buffer_}; | ||||
|   } | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) override; | ||||
|  | ||||
|  protected: | ||||
|   friend APIServer; | ||||
|  | ||||
|   void on_error_(int8_t error); | ||||
|   void on_disconnect_(); | ||||
|   void on_timeout_(uint32_t time); | ||||
|   void on_data_(uint8_t *buf, size_t len); | ||||
|   void parse_recv_buffer_(); | ||||
|   bool send_(const void *buf, size_t len, bool force); | ||||
|  | ||||
|   enum class ConnectionState { | ||||
|     WAITING_FOR_HELLO, | ||||
| @@ -147,8 +147,10 @@ class APIConnection : public APIServerConnection { | ||||
|  | ||||
|   bool remove_{false}; | ||||
|  | ||||
|   std::vector<uint8_t> send_buffer_; | ||||
|   std::vector<uint8_t> recv_buffer_; | ||||
|   // Buffer used to encode proto messages | ||||
|   // Re-use to prevent allocations | ||||
|   std::vector<uint8_t> proto_write_buffer_; | ||||
|   std::unique_ptr<APIFrameHelper> helper_; | ||||
|  | ||||
|   std::string client_info_; | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| @@ -160,12 +162,11 @@ class APIConnection : public APIServerConnection { | ||||
|   uint32_t last_traffic_; | ||||
|   bool sent_ping_{false}; | ||||
|   bool service_call_subscription_{false}; | ||||
|   bool current_nodelay_{false}; | ||||
|   bool next_close_{false}; | ||||
|   AsyncClient *client_; | ||||
|   bool next_close_ = false; | ||||
|   APIServer *parent_; | ||||
|   InitialStateIterator initial_state_iterator_; | ||||
|   ListEntitiesIterator list_entities_iterator_; | ||||
|   int state_subs_at_ = -1; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
							
								
								
									
										998
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										998
									
								
								esphome/components/api/api_frame_helper.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,998 @@ | ||||
| #include "api_frame_helper.h" | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "proto.h" | ||||
| #include <cstring> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| static const char *const TAG = "api.socket"; | ||||
|  | ||||
| /// Is the given return value (from read/write syscalls) a wouldblock error? | ||||
| bool is_would_block(ssize_t ret) { | ||||
|   if (ret == -1) { | ||||
|     return errno == EWOULDBLOCK || errno == EAGAIN; | ||||
|   } | ||||
|   return ret == 0; | ||||
| } | ||||
|  | ||||
| const char *api_error_to_str(APIError err) { | ||||
|   // not using switch to ensure compiler doesn't try to build a big table out of it | ||||
|   if (err == APIError::OK) { | ||||
|     return "OK"; | ||||
|   } else if (err == APIError::WOULD_BLOCK) { | ||||
|     return "WOULD_BLOCK"; | ||||
|   } else if (err == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||
|     return "BAD_HANDSHAKE_PACKET_LEN"; | ||||
|   } else if (err == APIError::BAD_INDICATOR) { | ||||
|     return "BAD_INDICATOR"; | ||||
|   } else if (err == APIError::BAD_DATA_PACKET) { | ||||
|     return "BAD_DATA_PACKET"; | ||||
|   } else if (err == APIError::TCP_NODELAY_FAILED) { | ||||
|     return "TCP_NODELAY_FAILED"; | ||||
|   } else if (err == APIError::TCP_NONBLOCKING_FAILED) { | ||||
|     return "TCP_NONBLOCKING_FAILED"; | ||||
|   } else if (err == APIError::CLOSE_FAILED) { | ||||
|     return "CLOSE_FAILED"; | ||||
|   } else if (err == APIError::SHUTDOWN_FAILED) { | ||||
|     return "SHUTDOWN_FAILED"; | ||||
|   } else if (err == APIError::BAD_STATE) { | ||||
|     return "BAD_STATE"; | ||||
|   } else if (err == APIError::BAD_ARG) { | ||||
|     return "BAD_ARG"; | ||||
|   } else if (err == APIError::SOCKET_READ_FAILED) { | ||||
|     return "SOCKET_READ_FAILED"; | ||||
|   } else if (err == APIError::SOCKET_WRITE_FAILED) { | ||||
|     return "SOCKET_WRITE_FAILED"; | ||||
|   } else if (err == APIError::HANDSHAKESTATE_READ_FAILED) { | ||||
|     return "HANDSHAKESTATE_READ_FAILED"; | ||||
|   } else if (err == APIError::HANDSHAKESTATE_WRITE_FAILED) { | ||||
|     return "HANDSHAKESTATE_WRITE_FAILED"; | ||||
|   } else if (err == APIError::HANDSHAKESTATE_BAD_STATE) { | ||||
|     return "HANDSHAKESTATE_BAD_STATE"; | ||||
|   } else if (err == APIError::CIPHERSTATE_DECRYPT_FAILED) { | ||||
|     return "CIPHERSTATE_DECRYPT_FAILED"; | ||||
|   } else if (err == APIError::CIPHERSTATE_ENCRYPT_FAILED) { | ||||
|     return "CIPHERSTATE_ENCRYPT_FAILED"; | ||||
|   } else if (err == APIError::OUT_OF_MEMORY) { | ||||
|     return "OUT_OF_MEMORY"; | ||||
|   } else if (err == APIError::HANDSHAKESTATE_SETUP_FAILED) { | ||||
|     return "HANDSHAKESTATE_SETUP_FAILED"; | ||||
|   } else if (err == APIError::HANDSHAKESTATE_SPLIT_FAILED) { | ||||
|     return "HANDSHAKESTATE_SPLIT_FAILED"; | ||||
|   } else if (err == APIError::BAD_HANDSHAKE_ERROR_BYTE) { | ||||
|     return "BAD_HANDSHAKE_ERROR_BYTE"; | ||||
|   } | ||||
|   return "UNKNOWN"; | ||||
| } | ||||
|  | ||||
| #define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__) | ||||
| // uncomment to log raw packets | ||||
| //#define HELPER_LOG_PACKETS | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| static const char *const PROLOGUE_INIT = "NoiseAPIInit"; | ||||
|  | ||||
| /// Convert a noise error code to a readable error | ||||
| std::string noise_err_to_str(int err) { | ||||
|   if (err == NOISE_ERROR_NO_MEMORY) | ||||
|     return "NO_MEMORY"; | ||||
|   if (err == NOISE_ERROR_UNKNOWN_ID) | ||||
|     return "UNKNOWN_ID"; | ||||
|   if (err == NOISE_ERROR_UNKNOWN_NAME) | ||||
|     return "UNKNOWN_NAME"; | ||||
|   if (err == NOISE_ERROR_MAC_FAILURE) | ||||
|     return "MAC_FAILURE"; | ||||
|   if (err == NOISE_ERROR_NOT_APPLICABLE) | ||||
|     return "NOT_APPLICABLE"; | ||||
|   if (err == NOISE_ERROR_SYSTEM) | ||||
|     return "SYSTEM"; | ||||
|   if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED) | ||||
|     return "REMOTE_KEY_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED) | ||||
|     return "LOCAL_KEY_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_PSK_REQUIRED) | ||||
|     return "PSK_REQUIRED"; | ||||
|   if (err == NOISE_ERROR_INVALID_LENGTH) | ||||
|     return "INVALID_LENGTH"; | ||||
|   if (err == NOISE_ERROR_INVALID_PARAM) | ||||
|     return "INVALID_PARAM"; | ||||
|   if (err == NOISE_ERROR_INVALID_STATE) | ||||
|     return "INVALID_STATE"; | ||||
|   if (err == NOISE_ERROR_INVALID_NONCE) | ||||
|     return "INVALID_NONCE"; | ||||
|   if (err == NOISE_ERROR_INVALID_PRIVATE_KEY) | ||||
|     return "INVALID_PRIVATE_KEY"; | ||||
|   if (err == NOISE_ERROR_INVALID_PUBLIC_KEY) | ||||
|     return "INVALID_PUBLIC_KEY"; | ||||
|   if (err == NOISE_ERROR_INVALID_FORMAT) | ||||
|     return "INVALID_FORMAT"; | ||||
|   if (err == NOISE_ERROR_INVALID_SIGNATURE) | ||||
|     return "INVALID_SIGNATURE"; | ||||
|   return to_string(err); | ||||
| } | ||||
|  | ||||
| /// Initialize the frame helper, returns OK if successful. | ||||
| APIError APINoiseFrameHelper::init() { | ||||
|   if (state_ != State::INITIALIZE || socket_ == nullptr) { | ||||
|     HELPER_LOG("Bad state for init %d", (int) state_); | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   int err = socket_->setblocking(false); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Setting nonblocking failed with errno %d", errno); | ||||
|     return APIError::TCP_NONBLOCKING_FAILED; | ||||
|   } | ||||
|  | ||||
|   int enable = 1; | ||||
|   err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Setting nodelay failed with errno %d", errno); | ||||
|     return APIError::TCP_NODELAY_FAILED; | ||||
|   } | ||||
|  | ||||
|   // init prologue | ||||
|   prologue_.insert(prologue_.end(), PROLOGUE_INIT, PROLOGUE_INIT + strlen(PROLOGUE_INIT)); | ||||
|  | ||||
|   state_ = State::CLIENT_HELLO; | ||||
|   return APIError::OK; | ||||
| } | ||||
| /// Run through handshake messages (if in that phase) | ||||
| APIError APINoiseFrameHelper::loop() { | ||||
|   APIError err = state_action_(); | ||||
|   if (err == APIError::WOULD_BLOCK) | ||||
|     return APIError::OK; | ||||
|   if (err != APIError::OK) | ||||
|     return err; | ||||
|   if (!tx_buf_.empty()) { | ||||
|     err = try_send_tx_buf_(); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
|  * | ||||
|  * @param frame: The struct to hold the frame information in. | ||||
|  *   msg_start: points to the start of the payload - this pointer is only valid until the next | ||||
|  *     try_receive_raw_ call | ||||
|  * | ||||
|  * @return 0 if a full packet is in rx_buf_ | ||||
|  * @return -1 if error, check errno. | ||||
|  * | ||||
|  * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later. | ||||
|  * errno ENOMEM: Not enough memory for reading packet. | ||||
|  * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||
|  * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   if (frame == nullptr) { | ||||
|     HELPER_LOG("Bad argument for try_read_frame_"); | ||||
|     return APIError::BAD_ARG; | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   if (rx_header_buf_len_ < 3) { | ||||
|     // no header information yet | ||||
|     size_t to_read = 3 - rx_header_buf_len_; | ||||
|     ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read); | ||||
|     if (is_would_block(received)) { | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } else if (received == -1) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket read failed with errno %d", errno); | ||||
|       return APIError::SOCKET_READ_FAILED; | ||||
|     } | ||||
|     rx_header_buf_len_ += received; | ||||
|     if (received != to_read) { | ||||
|       // not a full read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|  | ||||
|     // header reading done | ||||
|   } | ||||
|  | ||||
|   // read body | ||||
|   uint8_t indicator = rx_header_buf_[0]; | ||||
|   if (indicator != 0x01) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad indicator byte %u", indicator); | ||||
|     return APIError::BAD_INDICATOR; | ||||
|   } | ||||
|  | ||||
|   uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2]; | ||||
|  | ||||
|   if (state_ != State::DATA && msg_size > 128) { | ||||
|     // for handshake message only permit up to 128 bytes | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad packet len for handshake: %d", msg_size); | ||||
|     return APIError::BAD_HANDSHAKE_PACKET_LEN; | ||||
|   } | ||||
|  | ||||
|   // reserve space for body | ||||
|   if (rx_buf_.size() != msg_size) { | ||||
|     rx_buf_.resize(msg_size); | ||||
|   } | ||||
|  | ||||
|   if (rx_buf_len_ < msg_size) { | ||||
|     // more data to read | ||||
|     size_t to_read = msg_size - rx_buf_len_; | ||||
|     ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||
|     if (is_would_block(received)) { | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } else if (received == -1) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket read failed with errno %d", errno); | ||||
|       return APIError::SOCKET_READ_FAILED; | ||||
|     } | ||||
|     rx_buf_len_ += received; | ||||
|     if (received != to_read) { | ||||
|       // not all read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
| #endif | ||||
|   frame->msg = std::move(rx_buf_); | ||||
|   // consume msg | ||||
|   rx_buf_ = {}; | ||||
|   rx_buf_len_ = 0; | ||||
|   rx_header_buf_len_ = 0; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| /** To be called from read/write methods. | ||||
|  * | ||||
|  * This method runs through the internal handshake methods, if in that state. | ||||
|  * | ||||
|  * If the handshake is still active when this method returns and a read/write can't take place at | ||||
|  * the moment, returns WOULD_BLOCK. | ||||
|  * If an error occured, returns that error. Only returns OK if the transport is ready for data | ||||
|  * traffic. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::state_action_() { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|   if (state_ == State::INITIALIZE) { | ||||
|     HELPER_LOG("Bad state for method: %d", (int) state_); | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   if (state_ == State::CLIENT_HELLO) { | ||||
|     // waiting for client hello | ||||
|     ParsedFrame frame; | ||||
|     aerr = try_read_frame_(&frame); | ||||
|     if (aerr == APIError::BAD_INDICATOR) { | ||||
|       send_explicit_handshake_reject_("Bad indicator byte"); | ||||
|       return aerr; | ||||
|     } | ||||
|     if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||
|       send_explicit_handshake_reject_("Bad handshake packet len"); | ||||
|       return aerr; | ||||
|     } | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|     // ignore contents, may be used in future for flags | ||||
|     prologue_.push_back((uint8_t)(frame.msg.size() >> 8)); | ||||
|     prologue_.push_back((uint8_t) frame.msg.size()); | ||||
|     prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end()); | ||||
|  | ||||
|     state_ = State::SERVER_HELLO; | ||||
|   } | ||||
|   if (state_ == State::SERVER_HELLO) { | ||||
|     // send server hello | ||||
|     uint8_t msg[1]; | ||||
|     msg[0] = 0x01;  // chosen proto | ||||
|     aerr = write_frame_(msg, 1); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
|     // start handshake | ||||
|     aerr = init_handshake_(); | ||||
|     if (aerr != APIError::OK) | ||||
|       return aerr; | ||||
|  | ||||
|     state_ = State::HANDSHAKE; | ||||
|   } | ||||
|   if (state_ == State::HANDSHAKE) { | ||||
|     int action = noise_handshakestate_get_action(handshake_); | ||||
|     if (action == NOISE_ACTION_READ_MESSAGE) { | ||||
|       // waiting for handshake msg | ||||
|       ParsedFrame frame; | ||||
|       aerr = try_read_frame_(&frame); | ||||
|       if (aerr == APIError::BAD_INDICATOR) { | ||||
|         send_explicit_handshake_reject_("Bad indicator byte"); | ||||
|         return aerr; | ||||
|       } | ||||
|       if (aerr == APIError::BAD_HANDSHAKE_PACKET_LEN) { | ||||
|         send_explicit_handshake_reject_("Bad handshake packet len"); | ||||
|         return aerr; | ||||
|       } | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|  | ||||
|       if (frame.msg.empty()) { | ||||
|         send_explicit_handshake_reject_("Empty handshake message"); | ||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||
|       } else if (frame.msg[0] != 0x00) { | ||||
|         HELPER_LOG("Bad handshake error byte: %u", frame.msg[0]); | ||||
|         send_explicit_handshake_reject_("Bad handshake error byte"); | ||||
|         return APIError::BAD_HANDSHAKE_ERROR_BYTE; | ||||
|       } | ||||
|  | ||||
|       NoiseBuffer mbuf; | ||||
|       noise_buffer_init(mbuf); | ||||
|       noise_buffer_set_input(mbuf, frame.msg.data() + 1, frame.msg.size() - 1); | ||||
|       err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr); | ||||
|       if (err != 0) { | ||||
|         state_ = State::FAILED; | ||||
|         HELPER_LOG("noise_handshakestate_read_message failed: %s", noise_err_to_str(err).c_str()); | ||||
|         if (err == NOISE_ERROR_MAC_FAILURE) { | ||||
|           send_explicit_handshake_reject_("Handshake MAC failure"); | ||||
|         } else { | ||||
|           send_explicit_handshake_reject_("Handshake error"); | ||||
|         } | ||||
|         return APIError::HANDSHAKESTATE_READ_FAILED; | ||||
|       } | ||||
|  | ||||
|       aerr = check_handshake_finished_(); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|     } else if (action == NOISE_ACTION_WRITE_MESSAGE) { | ||||
|       uint8_t buffer[65]; | ||||
|       NoiseBuffer mbuf; | ||||
|       noise_buffer_init(mbuf); | ||||
|       noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1); | ||||
|  | ||||
|       err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr); | ||||
|       if (err != 0) { | ||||
|         state_ = State::FAILED; | ||||
|         HELPER_LOG("noise_handshakestate_write_message failed: %s", noise_err_to_str(err).c_str()); | ||||
|         return APIError::HANDSHAKESTATE_WRITE_FAILED; | ||||
|       } | ||||
|       buffer[0] = 0x00;  // success | ||||
|  | ||||
|       aerr = write_frame_(buffer, mbuf.size + 1); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|       aerr = check_handshake_finished_(); | ||||
|       if (aerr != APIError::OK) | ||||
|         return aerr; | ||||
|     } else { | ||||
|       // bad state for action | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad action for handshake: %d", action); | ||||
|       return APIError::HANDSHAKESTATE_BAD_STATE; | ||||
|     } | ||||
|   } | ||||
|   if (state_ == State::CLOSED || state_ == State::FAILED) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
| void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &reason) { | ||||
|   std::vector<uint8_t> data; | ||||
|   data.resize(reason.length() + 1); | ||||
|   data[0] = 0x01;  // failure | ||||
|   for (size_t i = 0; i < reason.length(); i++) { | ||||
|     data[i + 1] = (uint8_t) reason[i]; | ||||
|   } | ||||
|   // temporarily remove failed state | ||||
|   auto orig_state = state_; | ||||
|   state_ = State::EXPLICIT_REJECT; | ||||
|   write_frame_(data.data(), data.size()); | ||||
|   state_ = orig_state; | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|   aerr = state_action_(); | ||||
|   if (aerr != APIError::OK) { | ||||
|     return aerr; | ||||
|   } | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   ParsedFrame frame; | ||||
|   aerr = try_read_frame_(&frame); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   NoiseBuffer mbuf; | ||||
|   noise_buffer_init(mbuf); | ||||
|   noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size()); | ||||
|   err = noise_cipherstate_decrypt(recv_cipher_, &mbuf); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::CIPHERSTATE_DECRYPT_FAILED; | ||||
|   } | ||||
|  | ||||
|   size_t msg_size = mbuf.size; | ||||
|   uint8_t *msg_data = frame.msg.data(); | ||||
|   if (msg_size < 4) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad data packet: size %d too short", msg_size); | ||||
|     return APIError::BAD_DATA_PACKET; | ||||
|   } | ||||
|  | ||||
|   // uint16_t type; | ||||
|   // uint16_t data_len; | ||||
|   // uint8_t *data; | ||||
|   // uint8_t *padding;  zero or more bytes to fill up the rest of the packet | ||||
|   uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1]; | ||||
|   uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3]; | ||||
|   if (data_len > msg_size - 4) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size); | ||||
|     return APIError::BAD_DATA_PACKET; | ||||
|   } | ||||
|  | ||||
|   buffer->container = std::move(frame.msg); | ||||
|   buffer->data_offset = 4; | ||||
|   buffer->data_len = data_len; | ||||
|   buffer->type = type; | ||||
|   return APIError::OK; | ||||
| } | ||||
| bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||
| APIError APINoiseFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|   aerr = state_action_(); | ||||
|   if (aerr != APIError::OK) { | ||||
|     return aerr; | ||||
|   } | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   size_t padding = 0; | ||||
|   size_t msg_len = 4 + payload_len + padding; | ||||
|   size_t frame_len = 3 + msg_len + noise_cipherstate_get_mac_length(send_cipher_); | ||||
|   auto tmpbuf = std::unique_ptr<uint8_t[]>{new (std::nothrow) uint8_t[frame_len]}; | ||||
|   if (tmpbuf == nullptr) { | ||||
|     HELPER_LOG("Could not allocate for writing packet"); | ||||
|     return APIError::OUT_OF_MEMORY; | ||||
|   } | ||||
|  | ||||
|   tmpbuf[0] = 0x01;  // indicator | ||||
|   // tmpbuf[1], tmpbuf[2] to be set later | ||||
|   const uint8_t msg_offset = 3; | ||||
|   const uint8_t payload_offset = msg_offset + 4; | ||||
|   tmpbuf[msg_offset + 0] = (uint8_t)(type >> 8);  // type | ||||
|   tmpbuf[msg_offset + 1] = (uint8_t) type; | ||||
|   tmpbuf[msg_offset + 2] = (uint8_t)(payload_len >> 8);  // data_len | ||||
|   tmpbuf[msg_offset + 3] = (uint8_t) payload_len; | ||||
|   // copy data | ||||
|   std::copy(payload, payload + payload_len, &tmpbuf[payload_offset]); | ||||
|   // fill padding with zeros | ||||
|   std::fill(&tmpbuf[payload_offset + payload_len], &tmpbuf[frame_len], 0); | ||||
|  | ||||
|   NoiseBuffer mbuf; | ||||
|   noise_buffer_init(mbuf); | ||||
|   noise_buffer_set_inout(mbuf, &tmpbuf[msg_offset], msg_len, frame_len - msg_offset); | ||||
|   err = noise_cipherstate_encrypt(send_cipher_, &mbuf); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_cipherstate_encrypt failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::CIPHERSTATE_ENCRYPT_FAILED; | ||||
|   } | ||||
|  | ||||
|   size_t total_len = 3 + mbuf.size; | ||||
|   tmpbuf[1] = (uint8_t)(mbuf.size >> 8); | ||||
|   tmpbuf[2] = (uint8_t) mbuf.size; | ||||
|  | ||||
|   struct iovec iov; | ||||
|   iov.iov_base = &tmpbuf[0]; | ||||
|   iov.iov_len = total_len; | ||||
|  | ||||
|   // write raw to not have two packets sent if NAGLE disabled | ||||
|   return write_raw_(&iov, 1); | ||||
| } | ||||
| APIError APINoiseFrameHelper::try_send_tx_buf_() { | ||||
|   // try send from tx_buf | ||||
|   while (state_ != State::CLOSED && !tx_buf_.empty()) { | ||||
|     ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); | ||||
|     if (sent == -1) { | ||||
|       if (errno == EWOULDBLOCK || errno == EAGAIN) | ||||
|         break; | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|       return APIError::SOCKET_WRITE_FAILED; | ||||
|     } else if (sent == 0) { | ||||
|       break; | ||||
|     } | ||||
|     // TODO: inefficient if multiple packets in txbuf | ||||
|     // replace with deque of buffers | ||||
|     tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); | ||||
|   } | ||||
|  | ||||
|   return APIError::OK; | ||||
| } | ||||
| /** Write the data to the socket, or buffer it a write would block | ||||
|  * | ||||
|  * @param data The data to write | ||||
|  * @param len The length of data | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { | ||||
|   if (iovcnt == 0) | ||||
|     return APIError::OK; | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   size_t total_write_len = 0; | ||||
|   for (int i = 0; i < iovcnt; i++) { | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
| #endif | ||||
|     total_write_len += iov[i].iov_len; | ||||
|   } | ||||
|  | ||||
|   if (!tx_buf_.empty()) { | ||||
|     // try to empty tx_buf_ first | ||||
|     aerr = try_send_tx_buf_(); | ||||
|     if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) | ||||
|       return aerr; | ||||
|   } | ||||
|  | ||||
|   if (!tx_buf_.empty()) { | ||||
|     // tx buf not empty, can't write now because then stream would be inconsistent | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base), | ||||
|                      reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } | ||||
|  | ||||
|   ssize_t sent = socket_->writev(iov, iovcnt); | ||||
|   if (is_would_block(sent)) { | ||||
|     // operation would block, add buffer to tx_buf | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base), | ||||
|                      reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } else if (sent == -1) { | ||||
|     // an error occured | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|     return APIError::SOCKET_WRITE_FAILED; | ||||
|   } else if (sent != total_write_len) { | ||||
|     // partially sent, add end to tx_buf | ||||
|     size_t to_consume = sent; | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       if (to_consume >= iov[i].iov_len) { | ||||
|         to_consume -= iov[i].iov_len; | ||||
|       } else { | ||||
|         tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume, | ||||
|                        reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|         to_consume = 0; | ||||
|       } | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } | ||||
|   // fully sent | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) { | ||||
|   uint8_t header[3]; | ||||
|   header[0] = 0x01;  // indicator | ||||
|   header[1] = (uint8_t)(len >> 8); | ||||
|   header[2] = (uint8_t) len; | ||||
|  | ||||
|   struct iovec iov[2]; | ||||
|   iov[0].iov_base = header; | ||||
|   iov[0].iov_len = 3; | ||||
|   iov[1].iov_base = const_cast<uint8_t *>(data); | ||||
|   iov[1].iov_len = len; | ||||
|  | ||||
|   return write_raw_(iov, 2); | ||||
| } | ||||
|  | ||||
| /** Initiate the data structures for the handshake. | ||||
|  * | ||||
|  * @return 0 on success, -1 on error (check errno) | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::init_handshake_() { | ||||
|   int err; | ||||
|   memset(&nid_, 0, sizeof(nid_)); | ||||
|   // const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256"; | ||||
|   // err = noise_protocol_name_to_id(&nid_, proto, strlen(proto)); | ||||
|   nid_.pattern_id = NOISE_PATTERN_NN; | ||||
|   nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY; | ||||
|   nid_.dh_id = NOISE_DH_CURVE25519; | ||||
|   nid_.prefix_id = NOISE_PREFIX_STANDARD; | ||||
|   nid_.hybrid_id = NOISE_DH_NONE; | ||||
|   nid_.hash_id = NOISE_HASH_SHA256; | ||||
|   nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0; | ||||
|  | ||||
|   err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_handshakestate_new_by_id failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||
|   } | ||||
|  | ||||
|   const auto &psk = ctx_->get_psk(); | ||||
|   err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size()); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_handshakestate_set_pre_shared_key failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||
|   } | ||||
|  | ||||
|   err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size()); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_handshakestate_set_prologue failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||
|   } | ||||
|   // set_prologue copies it into handshakestate, so we can get rid of it now | ||||
|   prologue_ = {}; | ||||
|  | ||||
|   err = noise_handshakestate_start(handshake_); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_handshakestate_start failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::HANDSHAKESTATE_SETUP_FAILED; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::check_handshake_finished_() { | ||||
|   assert(state_ == State::HANDSHAKE); | ||||
|  | ||||
|   int action = noise_handshakestate_get_action(handshake_); | ||||
|   if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE) | ||||
|     return APIError::OK; | ||||
|   if (action != NOISE_ACTION_SPLIT) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Bad action for handshake: %d", action); | ||||
|     return APIError::HANDSHAKESTATE_BAD_STATE; | ||||
|   } | ||||
|   int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("noise_handshakestate_split failed: %s", noise_err_to_str(err).c_str()); | ||||
|     return APIError::HANDSHAKESTATE_SPLIT_FAILED; | ||||
|   } | ||||
|  | ||||
|   HELPER_LOG("Handshake complete!"); | ||||
|   noise_handshakestate_free(handshake_); | ||||
|   handshake_ = nullptr; | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APINoiseFrameHelper::~APINoiseFrameHelper() { | ||||
|   if (handshake_ != nullptr) { | ||||
|     noise_handshakestate_free(handshake_); | ||||
|     handshake_ = nullptr; | ||||
|   } | ||||
|   if (send_cipher_ != nullptr) { | ||||
|     noise_cipherstate_free(send_cipher_); | ||||
|     send_cipher_ = nullptr; | ||||
|   } | ||||
|   if (recv_cipher_ != nullptr) { | ||||
|     noise_cipherstate_free(recv_cipher_); | ||||
|     recv_cipher_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| APIError APINoiseFrameHelper::close() { | ||||
|   state_ = State::CLOSED; | ||||
|   int err = socket_->close(); | ||||
|   if (err == -1) | ||||
|     return APIError::CLOSE_FAILED; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APINoiseFrameHelper::shutdown(int how) { | ||||
|   int err = socket_->shutdown(how); | ||||
|   if (err == -1) | ||||
|     return APIError::SHUTDOWN_FAILED; | ||||
|   if (how == SHUT_RDWR) { | ||||
|     state_ = State::CLOSED; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
| extern "C" { | ||||
| // declare how noise generates random bytes (here with a good HWRNG based on the RF system) | ||||
| void noise_rand_bytes(void *output, size_t len) { esphome::fill_random(reinterpret_cast<uint8_t *>(output), len); } | ||||
| } | ||||
| #endif  // USE_API_NOISE | ||||
|  | ||||
| #ifdef USE_API_PLAINTEXT | ||||
|  | ||||
| /// Initialize the frame helper, returns OK if successful. | ||||
| APIError APIPlaintextFrameHelper::init() { | ||||
|   if (state_ != State::INITIALIZE || socket_ == nullptr) { | ||||
|     HELPER_LOG("Bad state for init %d", (int) state_); | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   int err = socket_->setblocking(false); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Setting nonblocking failed with errno %d", errno); | ||||
|     return APIError::TCP_NONBLOCKING_FAILED; | ||||
|   } | ||||
|   int enable = 1; | ||||
|   err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||
|   if (err != 0) { | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Setting nodelay failed with errno %d", errno); | ||||
|     return APIError::TCP_NODELAY_FAILED; | ||||
|   } | ||||
|  | ||||
|   state_ = State::DATA; | ||||
|   return APIError::OK; | ||||
| } | ||||
| /// Not used for plaintext | ||||
| APIError APIPlaintextFrameHelper::loop() { | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|   // try send pending TX data | ||||
|   if (!tx_buf_.empty()) { | ||||
|     APIError err = try_send_tx_buf_(); | ||||
|     if (err != APIError::OK) { | ||||
|       return err; | ||||
|     } | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| /** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter | ||||
|  * | ||||
|  * @param frame: The struct to hold the frame information in. | ||||
|  *   msg: store the parsed frame in that struct | ||||
|  * | ||||
|  * @return See APIError | ||||
|  * | ||||
|  * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame. | ||||
|  */ | ||||
| APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   if (frame == nullptr) { | ||||
|     HELPER_LOG("Bad argument for try_read_frame_"); | ||||
|     return APIError::BAD_ARG; | ||||
|   } | ||||
|  | ||||
|   // read header | ||||
|   while (!rx_header_parsed_) { | ||||
|     uint8_t data; | ||||
|     ssize_t received = socket_->read(&data, 1); | ||||
|     if (is_would_block(received)) { | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } else if (received == -1) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket read failed with errno %d", errno); | ||||
|       return APIError::SOCKET_READ_FAILED; | ||||
|     } | ||||
|     rx_header_buf_.push_back(data); | ||||
|  | ||||
|     // try parse header | ||||
|     if (rx_header_buf_[0] != 0x00) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]); | ||||
|       return APIError::BAD_INDICATOR; | ||||
|     } | ||||
|  | ||||
|     size_t i = 1; | ||||
|     uint32_t consumed = 0; | ||||
|     auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); | ||||
|     if (!msg_size_varint.has_value()) { | ||||
|       // not enough data there yet | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     i += consumed; | ||||
|     rx_header_parsed_len_ = msg_size_varint->as_uint32(); | ||||
|  | ||||
|     auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[i], rx_header_buf_.size() - i, &consumed); | ||||
|     if (!msg_type_varint.has_value()) { | ||||
|       // not enough data there yet | ||||
|       continue; | ||||
|     } | ||||
|     rx_header_parsed_type_ = msg_type_varint->as_uint32(); | ||||
|     rx_header_parsed_ = true; | ||||
|   } | ||||
|   // header reading done | ||||
|  | ||||
|   // reserve space for body | ||||
|   if (rx_buf_.size() != rx_header_parsed_len_) { | ||||
|     rx_buf_.resize(rx_header_parsed_len_); | ||||
|   } | ||||
|  | ||||
|   if (rx_buf_len_ < rx_header_parsed_len_) { | ||||
|     // more data to read | ||||
|     size_t to_read = rx_header_parsed_len_ - rx_buf_len_; | ||||
|     ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read); | ||||
|     if (is_would_block(received)) { | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } else if (received == -1) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket read failed with errno %d", errno); | ||||
|       return APIError::SOCKET_READ_FAILED; | ||||
|     } | ||||
|     rx_buf_len_ += received; | ||||
|     if (received != to_read) { | ||||
|       // not all read | ||||
|       return APIError::WOULD_BLOCK; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // uncomment for even more debugging | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|   ESP_LOGVV(TAG, "Received frame: %s", hexencode(rx_buf_).c_str()); | ||||
| #endif | ||||
|   frame->msg = std::move(rx_buf_); | ||||
|   // consume msg | ||||
|   rx_buf_ = {}; | ||||
|   rx_buf_len_ = 0; | ||||
|   rx_header_buf_.clear(); | ||||
|   rx_header_parsed_ = false; | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::WOULD_BLOCK; | ||||
|   } | ||||
|  | ||||
|   ParsedFrame frame; | ||||
|   aerr = try_read_frame_(&frame); | ||||
|   if (aerr != APIError::OK) | ||||
|     return aerr; | ||||
|  | ||||
|   buffer->container = std::move(frame.msg); | ||||
|   buffer->data_offset = 0; | ||||
|   buffer->data_len = rx_header_parsed_len_; | ||||
|   buffer->type = rx_header_parsed_type_; | ||||
|   return APIError::OK; | ||||
| } | ||||
| bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); } | ||||
| APIError APIPlaintextFrameHelper::write_packet(uint16_t type, const uint8_t *payload, size_t payload_len) { | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   if (state_ != State::DATA) { | ||||
|     return APIError::BAD_STATE; | ||||
|   } | ||||
|  | ||||
|   std::vector<uint8_t> header; | ||||
|   header.push_back(0x00); | ||||
|   ProtoVarInt(payload_len).encode(header); | ||||
|   ProtoVarInt(type).encode(header); | ||||
|  | ||||
|   struct iovec iov[2]; | ||||
|   iov[0].iov_base = &header[0]; | ||||
|   iov[0].iov_len = header.size(); | ||||
|   iov[1].iov_base = const_cast<uint8_t *>(payload); | ||||
|   iov[1].iov_len = payload_len; | ||||
|  | ||||
|   return write_raw_(iov, 2); | ||||
| } | ||||
| APIError APIPlaintextFrameHelper::try_send_tx_buf_() { | ||||
|   // try send from tx_buf | ||||
|   while (state_ != State::CLOSED && !tx_buf_.empty()) { | ||||
|     ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size()); | ||||
|     if (is_would_block(sent)) { | ||||
|       break; | ||||
|     } else if (sent == -1) { | ||||
|       state_ = State::FAILED; | ||||
|       HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|       return APIError::SOCKET_WRITE_FAILED; | ||||
|     } | ||||
|     // TODO: inefficient if multiple packets in txbuf | ||||
|     // replace with deque of buffers | ||||
|     tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent); | ||||
|   } | ||||
|  | ||||
|   return APIError::OK; | ||||
| } | ||||
| /** Write the data to the socket, or buffer it a write would block | ||||
|  * | ||||
|  * @param data The data to write | ||||
|  * @param len The length of data | ||||
|  */ | ||||
| APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { | ||||
|   if (iovcnt == 0) | ||||
|     return APIError::OK; | ||||
|   int err; | ||||
|   APIError aerr; | ||||
|  | ||||
|   size_t total_write_len = 0; | ||||
|   for (int i = 0; i < iovcnt; i++) { | ||||
| #ifdef HELPER_LOG_PACKETS | ||||
|     ESP_LOGVV(TAG, "Sending raw: %s", hexencode(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str()); | ||||
| #endif | ||||
|     total_write_len += iov[i].iov_len; | ||||
|   } | ||||
|  | ||||
|   if (!tx_buf_.empty()) { | ||||
|     // try to empty tx_buf_ first | ||||
|     aerr = try_send_tx_buf_(); | ||||
|     if (aerr != APIError::OK && aerr != APIError::WOULD_BLOCK) | ||||
|       return aerr; | ||||
|   } | ||||
|  | ||||
|   if (!tx_buf_.empty()) { | ||||
|     // tx buf not empty, can't write now because then stream would be inconsistent | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base), | ||||
|                      reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } | ||||
|  | ||||
|   ssize_t sent = socket_->writev(iov, iovcnt); | ||||
|   if (is_would_block(sent)) { | ||||
|     // operation would block, add buffer to tx_buf | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base), | ||||
|                      reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } else if (sent == -1) { | ||||
|     // an error occured | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|     return APIError::SOCKET_WRITE_FAILED; | ||||
|   } else if (sent != total_write_len) { | ||||
|     // partially sent, add end to tx_buf | ||||
|     size_t to_consume = sent; | ||||
|     for (int i = 0; i < iovcnt; i++) { | ||||
|       if (to_consume >= iov[i].iov_len) { | ||||
|         to_consume -= iov[i].iov_len; | ||||
|       } else { | ||||
|         tx_buf_.insert(tx_buf_.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume, | ||||
|                        reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len); | ||||
|         to_consume = 0; | ||||
|       } | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } | ||||
|   // fully sent | ||||
|   return APIError::OK; | ||||
| } | ||||
|  | ||||
| APIError APIPlaintextFrameHelper::close() { | ||||
|   state_ = State::CLOSED; | ||||
|   int err = socket_->close(); | ||||
|   if (err == -1) | ||||
|     return APIError::CLOSE_FAILED; | ||||
|   return APIError::OK; | ||||
| } | ||||
| APIError APIPlaintextFrameHelper::shutdown(int how) { | ||||
|   int err = socket_->shutdown(how); | ||||
|   if (err == -1) | ||||
|     return APIError::SHUTDOWN_FAILED; | ||||
|   if (how == SHUT_RDWR) { | ||||
|     state_ = State::CLOSED; | ||||
|   } | ||||
|   return APIError::OK; | ||||
| } | ||||
| #endif  // USE_API_PLAINTEXT | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user