mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	| @@ -25,10 +25,9 @@ indent_size = 2 | ||||
| [*.{yaml,yml}] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
| quote_type = single | ||||
| quote_type = double | ||||
|  | ||||
| # JSON | ||||
| [*.json] | ||||
| indent_style = space | ||||
| indent_size = 2 | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/FUNDING.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| --- | ||||
| # These are supported funding model platforms | ||||
|  | ||||
| custom: https://www.nabucasa.com | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/ISSUE_TEMPLATE/config.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,3 +1,4 @@ | ||||
| --- | ||||
| blank_issues_enabled: false | ||||
| contact_links: | ||||
|   - name: Issue Tracker | ||||
| @@ -5,7 +6,10 @@ contact_links: | ||||
|     about: Please create bug reports in the dedicated issue tracker. | ||||
|   - name: Feature Request Tracker | ||||
|     url: https://github.com/esphome/feature-requests | ||||
|     about: Please create feature requests in the dedicated feature request tracker. | ||||
|     about: | | ||||
|       Please create feature requests in the dedicated feature request tracker. | ||||
|   - name: Frequently Asked Question | ||||
|     url: https://esphome.io/guides/faq.html | ||||
|     about: Please view the FAQ for common questions and what to include in a bug report. | ||||
|     about: | | ||||
|       Please view the FAQ for common questions and what | ||||
|       to include in a bug report. | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,13 +1,14 @@ | ||||
| --- | ||||
| version: 2 | ||||
| updates: | ||||
|   - package-ecosystem: "pip" | ||||
|   - package-ecosystem: pip | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: "daily" | ||||
|       interval: daily | ||||
|     ignore: | ||||
|       # Hypotehsis is only used for testing and is updated quite often | ||||
|       - dependency-name: hypothesis | ||||
|   - package-ecosystem: "github-actions" | ||||
|   - package-ecosystem: github-actions | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: daily | ||||
|   | ||||
							
								
								
									
										56
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										56
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,21 +1,23 @@ | ||||
| --- | ||||
| name: CI for docker images | ||||
|  | ||||
| # Only run when docker paths change | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev, beta, release] | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|       - "docker/**" | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|  | ||||
|   pull_request: | ||||
|     paths: | ||||
|       - 'docker/**' | ||||
|       - '.github/workflows/**' | ||||
|       - 'requirements*.txt' | ||||
|       - 'platformio.ini' | ||||
|       - "docker/**" | ||||
|       - ".github/workflows/**" | ||||
|       - "requirements*.txt" | ||||
|       - "platformio.ini" | ||||
|  | ||||
| permissions: | ||||
|   contents: read | ||||
| @@ -30,24 +32,24 @@ jobs: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v2 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|         echo "TAG=check" >> $GITHUB_ENV | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|           echo "TAG=check" >> $GITHUB_ENV | ||||
|  | ||||
|     - name: Run build | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${TAG}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build | ||||
|       - name: Run build | ||||
|         run: | | ||||
|           docker/build.py \ | ||||
|             --tag "${TAG}" \ | ||||
|             --arch "${{ matrix.arch }}" \ | ||||
|             --build-type "${{ matrix.build_type }}" \ | ||||
|             build | ||||
|   | ||||
							
								
								
									
										21
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| --- | ||||
| name: CI | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   push: | ||||
|     branches: [dev, beta, release] | ||||
| @@ -10,6 +12,7 @@ permissions: | ||||
|   contents: read | ||||
|  | ||||
| concurrency: | ||||
|   # yamllint disable-line rule:line-length | ||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| @@ -73,6 +76,8 @@ jobs: | ||||
|             name: Run script/clang-tidy for ESP32 IDF | ||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||
|             pio_cache_key: tidyesp32-idf | ||||
|           - id: yamllint | ||||
|             name: Run yamllint | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v3 | ||||
| @@ -80,17 +85,19 @@ jobs: | ||||
|         uses: actions/setup-python@v4 | ||||
|         id: python | ||||
|         with: | ||||
|           python-version: '3.8' | ||||
|           python-version: "3.8" | ||||
|  | ||||
|       - name: Cache virtualenv | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: .venv | ||||
|           # yamllint disable-line rule:line-length | ||||
|           key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} | ||||
|           restore-keys: | | ||||
|             venv-${{ steps.python.outputs.python-version }}- | ||||
|  | ||||
|       - name: Set up virtualenv | ||||
|         # yamllint disable rule:line-length | ||||
|         run: | | ||||
|           python -m venv .venv | ||||
|           source .venv/bin/activate | ||||
| @@ -99,12 +106,14 @@ jobs: | ||||
|           pip install -e . | ||||
|           echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH | ||||
|           echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV | ||||
|         # yamllint enable rule:line-length | ||||
|  | ||||
|       # Use per check platformio cache because checks use different parts | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           # yamllint disable-line rule:line-length | ||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||
|         if: matrix.id == 'test' || matrix.id == 'clang-tidy' | ||||
|  | ||||
| @@ -145,8 +154,9 @@ jobs: | ||||
|           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. | ||||
|       # 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 | ||||
| @@ -161,6 +171,11 @@ jobs: | ||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||
|  | ||||
|       - name: Run yamllint | ||||
|         if: matrix.id == 'yamllint' | ||||
|         uses: frenck/action-yamllint@v1.3.0 | ||||
|  | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         # yamllint disable-line rule:line-length | ||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,10 @@ | ||||
| --- | ||||
| name: Lock | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|     - cron: "30 0 * * *" | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
|   | ||||
							
								
								
									
										104
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										104
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,5 +1,7 @@ | ||||
| --- | ||||
| name: Publish Release | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   workflow_dispatch: | ||||
|   release: | ||||
| @@ -20,6 +22,7 @@ jobs: | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         # yamllint disable rule:line-length | ||||
|         run: | | ||||
|           if [[ "$GITHUB_EVENT_NAME" = "release" ]]; then | ||||
|             TAG="${GITHUB_REF#refs/tags/}" | ||||
| @@ -29,6 +32,7 @@ jobs: | ||||
|             TAG="${TAG}${today}" | ||||
|           fi | ||||
|           echo "::set-output name=tag::${TAG}" | ||||
|         # yamllint enable rule:line-length | ||||
|  | ||||
|   deploy-pypi: | ||||
|     name: Build and publish to PyPi | ||||
| @@ -39,7 +43,7 @@ jobs: | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: '3.x' | ||||
|           python-version: "3.x" | ||||
|       - name: Set up python environment | ||||
|         run: | | ||||
|           script/setup | ||||
| @@ -65,37 +69,37 @@ jobs: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|  | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v2 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v2 | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v2 | ||||
|       - name: Set up QEMU | ||||
|         uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|         username: ${{ secrets.DOCKER_USER }} | ||||
|         password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|     - name: Log in to the GitHub container registry | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|     - name: Build and push | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --arch "${{ matrix.arch }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           build \ | ||||
|           --push | ||||
|       - name: 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' | ||||
| @@ -108,34 +112,34 @@ jobs: | ||||
|       matrix: | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Enable experimental manifest support | ||||
|       run: | | ||||
|         mkdir -p ~/.docker | ||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: "3.9" | ||||
|       - name: Enable experimental manifest support | ||||
|         run: | | ||||
|           mkdir -p ~/.docker | ||||
|           echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|         username: ${{ secrets.DOCKER_USER }} | ||||
|         password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|     - name: Log in to the GitHub container registry | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           username: ${{ secrets.DOCKER_USER }} | ||||
|           password: ${{ secrets.DOCKER_PASSWORD }} | ||||
|       - name: Log in to the GitHub container registry | ||||
|         uses: docker/login-action@v2 | ||||
|         with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|           password: ${{ secrets.GITHUB_TOKEN }} | ||||
|  | ||||
|     - name: Run manifest | ||||
|       run: | | ||||
|         docker/build.py \ | ||||
|           --tag "${{ needs.init.outputs.tag }}" \ | ||||
|           --build-type "${{ matrix.build_type }}" \ | ||||
|           manifest | ||||
|       - name: Run manifest | ||||
|         run: | | ||||
|           docker/build.py \ | ||||
|             --tag "${{ needs.init.outputs.tag }}" \ | ||||
|             --build-type "${{ matrix.build_type }}" \ | ||||
|             manifest | ||||
|  | ||||
|   deploy-ha-addon-repo: | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
| @@ -144,6 +148,7 @@ jobs: | ||||
|     steps: | ||||
|       - env: | ||||
|           TOKEN: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }} | ||||
|         # yamllint disable rule:line-length | ||||
|         run: | | ||||
|           TAG="${GITHUB_REF#refs/tags/}" | ||||
|           curl \ | ||||
| @@ -152,3 +157,4 @@ jobs: | ||||
|             -H "Accept: application/vnd.github.v3+json" \ | ||||
|             https://api.github.com/repos/esphome/home-assistant-addon/actions/workflows/bump-version.yml/dispatches \ | ||||
|             -d "{\"ref\":\"main\",\"inputs\":{\"version\":\"$TAG\"}}" | ||||
|         # yamllint enable rule:line-length | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,8 +1,10 @@ | ||||
| --- | ||||
| name: Stale | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: '30 0 * * *' | ||||
|     - cron: "30 0 * * *" | ||||
|   workflow_dispatch: | ||||
|  | ||||
| permissions: | ||||
| @@ -31,7 +33,8 @@ jobs: | ||||
|             and will be closed if no further activity occurs within 7 days. | ||||
|             Thank you for your contributions. | ||||
|  | ||||
|   # Use stale to automatically close issues with a reference to the issue tracker | ||||
|   # Use stale to automatically close issues with a | ||||
|   # reference to the issue tracker | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|   | ||||
							
								
								
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								.gitpod.yml
									
									
									
									
									
								
							| @@ -1,6 +1,8 @@ | ||||
| --- | ||||
| ports: | ||||
| - port: 6052 | ||||
|   onOpen: open-preview | ||||
|   - port: 6052 | ||||
|     onOpen: open-preview | ||||
| tasks: | ||||
| - before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup | ||||
|   command: python -m esphome dashboard config | ||||
|   # yamllint disable-line rule:line-length | ||||
|   - before: pyenv local $(pyenv version | grep '^3\.' | cut -d ' ' -f 1) && script/setup | ||||
|     command: python -m esphome dashboard config | ||||
|   | ||||
| @@ -1,14 +1,15 @@ | ||||
| --- | ||||
| # See https://pre-commit.com for more information | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/ambv/black | ||||
|     rev: 22.6.0 | ||||
|     hooks: | ||||
|     - id: black | ||||
|       args: | ||||
|         - --safe | ||||
|         - --quiet | ||||
|       files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|       - id: black | ||||
|         args: | ||||
|           - --safe | ||||
|           - --quiet | ||||
|         files: ^((esphome|script|tests)/.+)?[^/]+\.py$ | ||||
|   - repo: https://gitlab.com/pycqa/flake8 | ||||
|     rev: 4.0.1 | ||||
|     hooks: | ||||
|   | ||||
							
								
								
									
										12
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -29,11 +29,15 @@ esphome/components/b_parasite/* @rbaron | ||||
| esphome/components/ballu/* @bazuchan | ||||
| esphome/components/bang_bang/* @OttoWinter | ||||
| esphome/components/bedjet/* @jhansche | ||||
| esphome/components/bedjet/climate/* @jhansche | ||||
| esphome/components/bedjet/fan/* @jhansche | ||||
| esphome/components/bh1750/* @OttoWinter | ||||
| esphome/components/binary_sensor/* @esphome/core | ||||
| esphome/components/bl0939/* @ziceva | ||||
| esphome/components/bl0940/* @tobias- | ||||
| esphome/components/bl0942/* @dbuezas | ||||
| esphome/components/ble_client/* @buxtronix | ||||
| esphome/components/bluetooth_proxy/* @jesserockz | ||||
| esphome/components/bme680_bsec/* @trvrnrth | ||||
| esphome/components/bmp3xx/* @martgras | ||||
| esphome/components/button/* @esphome/core | ||||
| @@ -59,6 +63,7 @@ esphome/components/debug/* @OttoWinter | ||||
| esphome/components/delonghi/* @grob6000 | ||||
| esphome/components/dfplayer/* @glmnet | ||||
| esphome/components/dht/* @OttoWinter | ||||
| esphome/components/dps310/* @kbx81 | ||||
| esphome/components/ds1307/* @badbadc0ffee | ||||
| esphome/components/dsmr/* @glmnet @zuidwijk | ||||
| esphome/components/ektf2232/* @jesserockz | ||||
| @@ -72,6 +77,7 @@ esphome/components/esp32_improv/* @jesserockz | ||||
| esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| esphome/components/ezo/* @ssieb | ||||
| esphome/components/factory_reset/* @anatoly-savchenkov | ||||
| esphome/components/fastled_base/* @OttoWinter | ||||
| esphome/components/feedback/* @ianchi | ||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||
| @@ -120,6 +126,7 @@ esphome/components/mcp2515/* @danielschramm @mvturnho | ||||
| esphome/components/mcp3204/* @rsumner | ||||
| esphome/components/mcp4728/* @berfenger | ||||
| esphome/components/mcp47a1/* @jesserockz | ||||
| esphome/components/mcp9600/* @MrEditor97 | ||||
| esphome/components/mcp9808/* @k7hpn | ||||
| esphome/components/md5/* @esphome/core | ||||
| esphome/components/mdns/* @esphome/core | ||||
| @@ -138,6 +145,7 @@ esphome/components/modbus_controller/switch/* @martgras | ||||
| esphome/components/modbus_controller/text_sensor/* @martgras | ||||
| esphome/components/mopeka_ble/* @spbrogan | ||||
| esphome/components/mopeka_pro_check/* @spbrogan | ||||
| esphome/components/mpl3115a2/* @kbickar | ||||
| esphome/components/mpu6886/* @fabaff | ||||
| esphome/components/network/* @esphome/core | ||||
| esphome/components/nextion/* @senexcrenshaw | ||||
| @@ -220,7 +228,9 @@ esphome/components/teleinfo/* @0hax | ||||
| esphome/components/thermostat/* @kbx81 | ||||
| esphome/components/time/* @OttoWinter | ||||
| esphome/components/tlc5947/* @rnauber | ||||
| esphome/components/tm1621/* @Philippe12 | ||||
| esphome/components/tm1637/* @glmnet | ||||
| esphome/components/tm1638/* @skykingjwc | ||||
| esphome/components/tmp102/* @timsavage | ||||
| esphome/components/tmp117/* @Azimath | ||||
| esphome/components/tof10120/* @wstrzalka | ||||
| @@ -235,6 +245,8 @@ esphome/components/tuya/sensor/* @jesserockz | ||||
| esphome/components/tuya/switch/* @jesserockz | ||||
| esphome/components/tuya/text_sensor/* @dentra | ||||
| esphome/components/uart/* @esphome/core | ||||
| esphome/components/ufire_ec/* @pvizeli | ||||
| esphome/components/ufire_ise/* @pvizeli | ||||
| esphome/components/ultrasonic/* @OttoWinter | ||||
| esphome/components/version/* @esphome/core | ||||
| esphome/components/wake_on_lan/* @willwill2will54 | ||||
|   | ||||
| @@ -88,10 +88,12 @@ def main(): | ||||
|                 sys.exit(1) | ||||
|  | ||||
|     # detect channel from tag | ||||
|     match = re.match(r'^\d+\.\d+(?:\.\d+)?(b\d+)?$', args.tag) | ||||
|     match = re.match(r"^(\d+\.\d+)(?:\.\d+)?(b\d+)?$", args.tag) | ||||
|     major_minor_version = None | ||||
|     if match is None: | ||||
|         channel = CHANNEL_DEV | ||||
|     elif match.group(1) is None: | ||||
|     elif match.group(2) is None: | ||||
|         major_minor_version = match.group(1) | ||||
|         channel = CHANNEL_RELEASE | ||||
|     else: | ||||
|         channel = CHANNEL_BETA | ||||
| @@ -106,6 +108,11 @@ def main(): | ||||
|         tags_to_push.append("beta") | ||||
|         tags_to_push.append("latest") | ||||
|  | ||||
|         # Compatibility with HA tags | ||||
|         if major_minor_version: | ||||
|             tags_to_push.append("stable") | ||||
|             tags_to_push.append(major_minor_version) | ||||
|  | ||||
|     if args.command == "build": | ||||
|         # 1. pull cache image | ||||
|         params = DockerParams.for_type_arch(args.build_type, args.arch) | ||||
|   | ||||
| @@ -4,33 +4,15 @@ | ||||
| //  - Arduino - AM2320: https://github.com/EngDial/AM2320/blob/master/src/AM2320.cpp | ||||
|  | ||||
| #include "am2320.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace am2320 { | ||||
|  | ||||
| static const char *const TAG = "am2320"; | ||||
|  | ||||
| // ---=== Calc CRC16 ===--- | ||||
| uint16_t crc_16(uint8_t *ptr, uint8_t length) { | ||||
|   uint16_t crc = 0xFFFF; | ||||
|   uint8_t i; | ||||
|   //------------------------------ | ||||
|   while (length--) { | ||||
|     crc ^= *ptr++; | ||||
|     for (i = 0; i < 8; i++) { | ||||
|       if ((crc & 0x01) != 0) { | ||||
|         crc >>= 1; | ||||
|         crc ^= 0xA001; | ||||
|       } else { | ||||
|         crc >>= 1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return crc; | ||||
| } | ||||
|  | ||||
| void AM2320Component::update() { | ||||
|   uint8_t data[8]; | ||||
|   data[0] = 0; | ||||
| @@ -98,7 +80,7 @@ bool AM2320Component::read_data_(uint8_t *data) { | ||||
|   checksum = data[7] << 8; | ||||
|   checksum += data[6]; | ||||
|  | ||||
|   if (crc_16(data, 6) != checksum) { | ||||
|   if (crc16(data, 6) != checksum) { | ||||
|     ESP_LOGW(TAG, "AM2320 Checksum invalid!"); | ||||
|     return false; | ||||
|   } | ||||
|   | ||||
| @@ -8,6 +8,27 @@ AUTO_LOAD = ["sensor", "binary_sensor"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| CONF_APDS9960_ID = "apds9960_id" | ||||
| CONF_LED_DRIVE = "led_drive" | ||||
| CONF_PROXIMITY_GAIN = "proximity_gain" | ||||
| CONF_AMBIENT_LIGHT_GAIN = "ambient_light_gain" | ||||
| CONF_GESTURE_LED_DRIVE = "gesture_led_drive" | ||||
| CONF_GESTURE_GAIN = "gesture_gain" | ||||
| CONF_GESTURE_WAIT_TIME = "gesture_wait_time" | ||||
|  | ||||
| DRIVE_LEVELS = {"100ma": 0, "50ma": 1, "25ma": 2, "12.5ma": 3} | ||||
| PROXIMITY_LEVELS = {"1x": 0, "2x": 1, "4x": 2, "8x": 3} | ||||
| AMBIENT_LEVELS = {"1x": 0, "4x": 1, "16x": 2, "64x": 3} | ||||
| GESTURE_LEVELS = {"1x": 0, "2x": 1, "4x": 2, "8x": 3} | ||||
| GESTURE_WAIT_TIMES = { | ||||
|     "0ms": 0, | ||||
|     "2.8ms": 1, | ||||
|     "5.6ms": 2, | ||||
|     "8.4ms": 3, | ||||
|     "14ms": 4, | ||||
|     "22.4ms": 5, | ||||
|     "30.8ms": 6, | ||||
|     "39.2ms": 7, | ||||
| } | ||||
|  | ||||
| apds9960_nds = cg.esphome_ns.namespace("apds9960") | ||||
| APDS9960 = apds9960_nds.class_("APDS9960", cg.PollingComponent, i2c.I2CDevice) | ||||
| @@ -16,6 +37,20 @@ CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(APDS9960), | ||||
|             cv.Optional(CONF_LED_DRIVE, "100mA"): cv.enum(DRIVE_LEVELS, lower=True), | ||||
|             cv.Optional(CONF_PROXIMITY_GAIN, "4x"): cv.enum( | ||||
|                 PROXIMITY_LEVELS, lower=True | ||||
|             ), | ||||
|             cv.Optional(CONF_AMBIENT_LIGHT_GAIN, "4x"): cv.enum( | ||||
|                 AMBIENT_LEVELS, lower=True | ||||
|             ), | ||||
|             cv.Optional(CONF_GESTURE_LED_DRIVE, "100mA"): cv.enum( | ||||
|                 DRIVE_LEVELS, lower=True | ||||
|             ), | ||||
|             cv.Optional(CONF_GESTURE_GAIN, "4x"): cv.enum(GESTURE_LEVELS, lower=True), | ||||
|             cv.Optional(CONF_GESTURE_WAIT_TIME, "2.8ms"): cv.enum( | ||||
|                 GESTURE_WAIT_TIMES, lower=True | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| @@ -27,3 +62,9 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|     cg.add(var.set_led_drive(config[CONF_LED_DRIVE])) | ||||
|     cg.add(var.set_proximity_gain(config[CONF_PROXIMITY_GAIN])) | ||||
|     cg.add(var.set_ambient_gain(config[CONF_AMBIENT_LIGHT_GAIN])) | ||||
|     cg.add(var.set_gesture_led_drive(config[CONF_GESTURE_LED_DRIVE])) | ||||
|     cg.add(var.set_gesture_gain(config[CONF_GESTURE_GAIN])) | ||||
|     cg.add(var.set_gesture_wait_time(config[CONF_GESTURE_WAIT_TIME])) | ||||
|   | ||||
| @@ -46,16 +46,16 @@ void APDS9960::setup() { | ||||
|   uint8_t val = 0; | ||||
|   APDS9960_ERROR_CHECK(this->read_byte(0x8F, &val)); | ||||
|   val &= 0b00111111; | ||||
|   uint8_t led_drive = 0;  // led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA | ||||
|   val |= (led_drive & 0b11) << 6; | ||||
|   // led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA | ||||
|   val |= (this->led_drive_ & 0b11) << 6; | ||||
|  | ||||
|   val &= 0b11110011; | ||||
|   uint8_t proximity_gain = 2;  // proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 4 -> 8X | ||||
|   val |= (proximity_gain & 0b11) << 2; | ||||
|   // proximity gain, 0 -> 1x, 1 -> 2X, 2 -> 4X, 3 -> 8X | ||||
|   val |= (this->proximity_gain_ & 0b11) << 2; | ||||
|  | ||||
|   val &= 0b11111100; | ||||
|   uint8_t ambient_gain = 1;  // ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x | ||||
|   val |= (ambient_gain & 0b11) << 0; | ||||
|   // ambient light gain, 0 -> 1x, 1 -> 4x, 2 -> 16x, 3 -> 64x | ||||
|   val |= (this->ambient_gain_ & 0b11) << 0; | ||||
|   APDS9960_WRITE_BYTE(0x8F, val); | ||||
|  | ||||
|   // Pers (0x8C) -> 0x11 (2 consecutive proximity or ALS for interrupt) | ||||
| @@ -75,19 +75,18 @@ void APDS9960::setup() { | ||||
|   // GConf 2 (0xA3, gesture config 2) -> | ||||
|   APDS9960_ERROR_CHECK(this->read_byte(0xA3, &val)); | ||||
|   val &= 0b10011111; | ||||
|   uint8_t gesture_gain = 2;  // gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x | ||||
|   val |= (gesture_gain & 0b11) << 5; | ||||
|   // gesture gain, 0 -> 1x, 1 -> 2x, 2 -> 4x, 3 -> 8x | ||||
|   val |= (this->gesture_gain_ & 0b11) << 5; | ||||
|  | ||||
|   val &= 0b11100111; | ||||
|   uint8_t gesture_led_drive = 0;  // gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA | ||||
|   val |= (gesture_led_drive & 0b11) << 3; | ||||
|   // gesture led drive, 0 -> 100mA, 1 -> 50mA, 2 -> 25mA, 3 -> 12.5mA | ||||
|   val |= (this->gesture_led_drive_ & 0b11) << 3; | ||||
|  | ||||
|   val &= 0b11111000; | ||||
|   // gesture wait time | ||||
|   // 0 -> 0ms, 1 -> 2.8ms, 2 -> 5.6ms, 3 -> 8.4ms | ||||
|   // 4 -> 14.0ms, 5 -> 22.4 ms, 6 -> 30.8ms, 7 -> 39.2 ms | ||||
|   uint8_t gesture_wait_time = 1;  // gesture wait time | ||||
|   val |= (gesture_wait_time & 0b111) << 0; | ||||
|   val |= (this->gesture_wait_time_ & 0b111) << 0; | ||||
|   APDS9960_WRITE_BYTE(0xA3, val); | ||||
|  | ||||
|   // GOffsetU (0xA4) -> 0x00 (no offset) | ||||
|   | ||||
| @@ -16,6 +16,13 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { | ||||
|   void update() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   void set_led_drive(uint8_t level) { this->led_drive_ = level; } | ||||
|   void set_proximity_gain(uint8_t gain) { this->proximity_gain_ = gain; } | ||||
|   void set_ambient_gain(uint8_t gain) { this->ambient_gain_ = gain; } | ||||
|   void set_gesture_led_drive(uint8_t level) { this->gesture_led_drive_ = level; } | ||||
|   void set_gesture_gain(uint8_t gain) { this->gesture_gain_ = gain; } | ||||
|   void set_gesture_wait_time(uint8_t wait_time) { this->gesture_wait_time_ = wait_time; } | ||||
|  | ||||
|   void set_red_channel(sensor::Sensor *red_channel) { red_channel_ = red_channel; } | ||||
|   void set_green_channel(sensor::Sensor *green_channel) { green_channel_ = green_channel; } | ||||
|   void set_blue_channel(sensor::Sensor *blue_channel) { blue_channel_ = blue_channel; } | ||||
| @@ -36,6 +43,13 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice { | ||||
|   void report_gesture_(int gesture); | ||||
|   void process_dataset_(int up, int down, int left, int right); | ||||
|  | ||||
|   uint8_t led_drive_; | ||||
|   uint8_t proximity_gain_; | ||||
|   uint8_t ambient_gain_; | ||||
|   uint8_t gesture_led_drive_; | ||||
|   uint8_t gesture_gain_; | ||||
|   uint8_t gesture_wait_time_; | ||||
|  | ||||
|   sensor::Sensor *red_channel_{nullptr}; | ||||
|   sensor::Sensor *green_channel_{nullptr}; | ||||
|   sensor::Sensor *blue_channel_{nullptr}; | ||||
|   | ||||
| @@ -27,6 +27,7 @@ service APIConnection { | ||||
|   rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} | ||||
|   rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} | ||||
|   rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} | ||||
|   rpc subscribe_bluetooth_le_advertisements (SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} | ||||
|   rpc get_time (GetTimeRequest) returns (GetTimeResponse) { | ||||
|     option (needs_authentication) = false; | ||||
|   } | ||||
| @@ -190,6 +191,8 @@ message DeviceInfoResponse { | ||||
|   string project_version = 9; | ||||
|  | ||||
|   uint32 webserver_port = 10; | ||||
|  | ||||
|   bool has_bluetooth_proxy = 11; | ||||
| } | ||||
|  | ||||
| message ListEntitiesRequest { | ||||
| @@ -1099,3 +1102,28 @@ message MediaPlayerCommandRequest { | ||||
|   bool has_media_url = 6; | ||||
|   string media_url = 7; | ||||
| } | ||||
|  | ||||
| // ==================== BLUETOOTH ==================== | ||||
| message SubscribeBluetoothLEAdvertisementsRequest { | ||||
|   option (id) = 66; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
| } | ||||
|  | ||||
| message BluetoothServiceData { | ||||
|   string uuid = 1; | ||||
|   repeated uint32 data = 2 [packed=false]; | ||||
| } | ||||
| message BluetoothLEAdvertisementResponse { | ||||
|   option (id) = 67; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   uint64 address = 1; | ||||
|   string name = 2; | ||||
|   sint32 rssi = 3; | ||||
|  | ||||
|   repeated string service_uuids = 4; | ||||
|   repeated BluetoothServiceData service_data = 5; | ||||
|   repeated BluetoothServiceData manufacturer_data = 6; | ||||
| } | ||||
|   | ||||
| @@ -886,6 +886,9 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | ||||
| #endif | ||||
| #ifdef USE_WEBSERVER | ||||
|   resp.webserver_port = USE_WEBSERVER_PORT; | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   resp.has_bluetooth_proxy = true; | ||||
| #endif | ||||
|   return resp; | ||||
| } | ||||
|   | ||||
| @@ -7,6 +7,10 @@ | ||||
| #include "api_server.h" | ||||
| #include "api_frame_helper.h" | ||||
|  | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| #include "esphome/components/bluetooth_proxy/bluetooth_proxy.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
|  | ||||
| @@ -94,6 +98,13 @@ class APIConnection : public APIServerConnection { | ||||
|       return; | ||||
|     this->send_homeassistant_service_response(call); | ||||
|   } | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { | ||||
|     if (!this->bluetooth_le_advertisement_subscription_) | ||||
|       return false; | ||||
|     return this->send_bluetooth_le_advertisement_response(call); | ||||
|   } | ||||
| #endif | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void send_time_request() { | ||||
|     GetTimeRequest req; | ||||
| @@ -134,6 +145,9 @@ class APIConnection : public APIServerConnection { | ||||
|     return {}; | ||||
|   } | ||||
|   void execute_service(const ExecuteServiceRequest &msg) override; | ||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { | ||||
|     this->bluetooth_le_advertisement_subscription_ = true; | ||||
|   } | ||||
|   bool is_authenticated() override { return this->connection_state_ == ConnectionState::AUTHENTICATED; } | ||||
|   bool is_connection_setup() override { | ||||
|     return this->connection_state_ == ConnectionState ::CONNECTED || this->is_authenticated(); | ||||
| @@ -176,6 +190,7 @@ class APIConnection : public APIServerConnection { | ||||
|   uint32_t last_traffic_; | ||||
|   bool sent_ping_{false}; | ||||
|   bool service_call_subscription_{false}; | ||||
|   bool bluetooth_le_advertisement_subscription_{true}; | ||||
|   bool next_close_ = false; | ||||
|   APIServer *parent_; | ||||
|   InitialStateIterator initial_state_iterator_; | ||||
|   | ||||
| @@ -495,6 +495,10 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|       this->webserver_port = value.as_uint32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 11: { | ||||
|       this->has_bluetooth_proxy = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -544,6 +548,7 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(8, this->project_name); | ||||
|   buffer.encode_string(9, this->project_version); | ||||
|   buffer.encode_uint32(10, this->webserver_port); | ||||
|   buffer.encode_bool(11, this->has_bluetooth_proxy); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
| @@ -589,6 +594,10 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   sprintf(buffer, "%u", this->webserver_port); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_bluetooth_proxy: "); | ||||
|   out.append(YESNO(this->has_bluetooth_proxy)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| @@ -4854,6 +4863,143 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { | ||||
|   out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); | ||||
| } | ||||
| #endif | ||||
| bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->data.push_back(value.as_uint32()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool BluetoothServiceData::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->uuid = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void BluetoothServiceData::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->uuid); | ||||
|   for (auto &it : this->data) { | ||||
|     buffer.encode_uint32(2, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void BluetoothServiceData::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("BluetoothServiceData {\n"); | ||||
|   out.append("  uuid: "); | ||||
|   out.append("'").append(this->uuid).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->data) { | ||||
|     out.append("  data: "); | ||||
|     sprintf(buffer, "%u", it); | ||||
|     out.append(buffer); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool BluetoothLEAdvertisementResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->address = value.as_uint64(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->rssi = value.as_sint32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool BluetoothLEAdvertisementResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->service_uuids.push_back(value.as_string()); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->service_data.push_back(value.as_message<BluetoothServiceData>()); | ||||
|       return true; | ||||
|     } | ||||
|     case 6: { | ||||
|       this->manufacturer_data.push_back(value.as_message<BluetoothServiceData>()); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void BluetoothLEAdvertisementResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_uint64(1, this->address); | ||||
|   buffer.encode_string(2, this->name); | ||||
|   buffer.encode_sint32(3, this->rssi); | ||||
|   for (auto &it : this->service_uuids) { | ||||
|     buffer.encode_string(4, it, true); | ||||
|   } | ||||
|   for (auto &it : this->service_data) { | ||||
|     buffer.encode_message<BluetoothServiceData>(5, it, true); | ||||
|   } | ||||
|   for (auto &it : this->manufacturer_data) { | ||||
|     buffer.encode_message<BluetoothServiceData>(6, it, true); | ||||
|   } | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("BluetoothLEAdvertisementResponse {\n"); | ||||
|   out.append("  address: "); | ||||
|   sprintf(buffer, "%llu", this->address); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  rssi: "); | ||||
|   sprintf(buffer, "%d", this->rssi); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   for (const auto &it : this->service_uuids) { | ||||
|     out.append("  service_uuids: "); | ||||
|     out.append("'").append(it).append("'"); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   for (const auto &it : this->service_data) { | ||||
|     out.append("  service_data: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|  | ||||
|   for (const auto &it : this->manufacturer_data) { | ||||
|     out.append("  manufacturer_data: "); | ||||
|     it.dump_to(out); | ||||
|     out.append("\n"); | ||||
|   } | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -263,6 +263,7 @@ class DeviceInfoResponse : public ProtoMessage { | ||||
|   std::string project_name{}; | ||||
|   std::string project_version{}; | ||||
|   uint32_t webserver_port{0}; | ||||
|   bool has_bluetooth_proxy{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| @@ -1214,6 +1215,45 @@ class MediaPlayerCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { | ||||
|  public: | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| class BluetoothServiceData : public ProtoMessage { | ||||
|  public: | ||||
|   std::string uuid{}; | ||||
|   std::vector<uint32_t> data{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class BluetoothLEAdvertisementResponse : public ProtoMessage { | ||||
|  public: | ||||
|   uint64_t address{0}; | ||||
|   std::string name{}; | ||||
|   int32_t rssi{0}; | ||||
|   std::vector<std::string> service_uuids{}; | ||||
|   std::vector<BluetoothServiceData> service_data{}; | ||||
|   std::vector<BluetoothServiceData> manufacturer_data{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -328,6 +328,14 @@ bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayer | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_bluetooth_le_advertisement_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<BluetoothLEAdvertisementResponse>(msg, 67); | ||||
| } | ||||
| #endif | ||||
| bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | ||||
|   switch (msg_type) { | ||||
|     case 1: { | ||||
| @@ -595,6 +603,15 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 66: { | ||||
|       SubscribeBluetoothLEAdvertisementsRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_subscribe_bluetooth_le_advertisements_request(msg); | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| @@ -691,6 +708,18 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc | ||||
|   } | ||||
|   this->subscribe_home_assistant_states(msg); | ||||
| } | ||||
| void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( | ||||
|     const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_bluetooth_le_advertisements(msg); | ||||
| } | ||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|   | ||||
| @@ -153,6 +153,11 @@ class APIServerConnectionBase : public ProtoService { | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; | ||||
| #endif | ||||
|   virtual void on_subscribe_bluetooth_le_advertisements_request( | ||||
|       const SubscribeBluetoothLEAdvertisementsRequest &value){}; | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); | ||||
| #endif | ||||
|  protected: | ||||
|   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; | ||||
| @@ -170,6 +175,7 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0; | ||||
|   virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0; | ||||
|   virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0; | ||||
|   virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0; | ||||
|   virtual GetTimeResponse get_time(const GetTimeRequest &msg) = 0; | ||||
|   virtual void execute_service(const ExecuteServiceRequest &msg) = 0; | ||||
| #ifdef USE_COVER | ||||
| @@ -216,6 +222,7 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
|   void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override; | ||||
|   void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override; | ||||
|   void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override; | ||||
|   void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||
|   void on_get_time_request(const GetTimeRequest &msg) override; | ||||
|   void on_execute_service_request(const ExecuteServiceRequest &msg) override; | ||||
| #ifdef USE_COVER | ||||
|   | ||||
| @@ -291,6 +291,13 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon | ||||
|     client->send_homeassistant_service_call(call); | ||||
|   } | ||||
| } | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { | ||||
|   for (auto &client : this->clients_) { | ||||
|     client->send_bluetooth_le_advertisement(call); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| APIServer::APIServer() { global_api_server = this; } | ||||
| void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||
|                                                std::function<void(std::string)> f) { | ||||
|   | ||||
| @@ -73,6 +73,9 @@ class APIServer : public Component, public Controller { | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
|   void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); | ||||
| #endif | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
|   void request_time(); | ||||
|   | ||||
| @@ -70,7 +70,7 @@ class ProtoVarInt { | ||||
|     } | ||||
|   } | ||||
|   void encode(std::vector<uint8_t> &out) { | ||||
|     uint32_t val = this->value_; | ||||
|     uint64_t val = this->value_; | ||||
|     if (val <= 0x7F) { | ||||
|       out.push_back(val); | ||||
|       return; | ||||
|   | ||||
| @@ -89,8 +89,10 @@ enum BedjetCommand : uint8_t { | ||||
|         "85%", "90%", "95%", "100%" \ | ||||
|   } | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[20] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const uint8_t BEDJET_FAN_SPEED_COUNT = 20; | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_; | ||||
| static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_; | ||||
|  | ||||
| }  // namespace bedjet | ||||
|   | ||||
| @@ -9,19 +9,17 @@ from esphome.const import ( | ||||
|     CONF_RECEIVE_TIMEOUT, | ||||
|     CONF_TIME_ID, | ||||
| ) | ||||
| from . import ( | ||||
| from .. import ( | ||||
|     BEDJET_CLIENT_SCHEMA, | ||||
|     bedjet_ns, | ||||
|     register_bedjet_child, | ||||
| ) | ||||
| 
 | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| CODEOWNERS = ["@jhansche"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| DEPENDENCIES = ["bedjet"] | ||||
| 
 | ||||
| bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||
| BedJetClimate = bedjet_ns.class_( | ||||
|     "BedJetClimate", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||
| ) | ||||
| BedJetClimate = bedjet_ns.class_("BedJetClimate", climate.Climate, cg.PollingComponent) | ||||
| BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") | ||||
| BEDJET_HEAT_MODES = { | ||||
|     "heat": BedjetHeatMode.HEAT_MODE_HEAT, | ||||
| @@ -15,13 +15,13 @@ float bedjet_temp_to_c(const uint8_t temp) { | ||||
| } | ||||
| 
 | ||||
| static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { | ||||
|   if (fan_step <= 19) | ||||
|   if (fan_step < BEDJET_FAN_SPEED_COUNT) | ||||
|     return &BEDJET_FAN_STEP_NAME_STRINGS[fan_step]; | ||||
|   return nullptr; | ||||
| } | ||||
| 
 | ||||
| static uint8_t bedjet_fan_speed_to_step(const std::string &fan_step_percent) { | ||||
|   for (int i = 0; i < sizeof(BEDJET_FAN_STEP_NAME_STRINGS); i++) { | ||||
|   for (int i = 0; i < BEDJET_FAN_SPEED_COUNT; i++) { | ||||
|     if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { | ||||
|       return i; | ||||
|     } | ||||
| @@ -1,12 +1,12 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "bedjet_child.h" | ||||
| #include "bedjet_codec.h" | ||||
| #include "bedjet_hub.h" | ||||
| #include "esphome/components/bedjet/bedjet_child.h" | ||||
| #include "esphome/components/bedjet/bedjet_codec.h" | ||||
| #include "esphome/components/bedjet/bedjet_hub.h" | ||||
| #include "esphome/components/climate/climate.h" | ||||
| 
 | ||||
| #ifdef USE_ESP32 | ||||
| 
 | ||||
							
								
								
									
										36
									
								
								esphome/components/bedjet/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								esphome/components/bedjet/fan/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,36 @@ | ||||
| import logging | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import fan | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
| ) | ||||
| from .. import ( | ||||
|     BEDJET_CLIENT_SCHEMA, | ||||
|     bedjet_ns, | ||||
|     register_bedjet_child, | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| CODEOWNERS = ["@jhansche"] | ||||
| DEPENDENCIES = ["bedjet"] | ||||
|  | ||||
| BedJetFan = bedjet_ns.class_("BedJetFan", fan.Fan, cg.PollingComponent) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     fan.FAN_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BedJetFan), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(BEDJET_CLIENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await fan.register_fan(var, config) | ||||
|     await register_bedjet_child(var, config) | ||||
							
								
								
									
										108
									
								
								esphome/components/bedjet/fan/bedjet_fan.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								esphome/components/bedjet/fan/bedjet_fan.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| #include "bedjet_fan.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| using namespace esphome::fan; | ||||
|  | ||||
| void BedJetFan::dump_config() { LOG_FAN("", "BedJet Fan", this); } | ||||
| std::string BedJetFan::describe() { return "BedJet Fan"; } | ||||
|  | ||||
| void BedJetFan::control(const fan::FanCall &call) { | ||||
|   ESP_LOGD(TAG, "Received BedJetFan::control"); | ||||
|   if (!this->parent_->is_connected()) { | ||||
|     ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); | ||||
|     return; | ||||
|   } | ||||
|   bool did_change = false; | ||||
|  | ||||
|   if (call.get_state().has_value() && this->state != *call.get_state()) { | ||||
|     // Turning off is easy: | ||||
|     if (this->state && this->parent_->button_off()) { | ||||
|       this->state = false; | ||||
|       this->publish_state(); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Turning on, we have to choose a specific mode; for now, use "COOL" mode | ||||
|     // In the future we could configure the mode to use for fan.turn_on. | ||||
|     if (this->parent_->button_cool()) { | ||||
|       this->state = true; | ||||
|       did_change = true; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // ignore speed changes if not on or turning on | ||||
|   if (this->state && call.get_speed().has_value()) { | ||||
|     this->speed = *call.get_speed(); | ||||
|     this->parent_->set_fan_index(this->speed); | ||||
|     did_change = true; | ||||
|   } | ||||
|  | ||||
|   if (did_change) { | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetFan::on_status(const BedjetStatusPacket *data) { | ||||
|   ESP_LOGVV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data); | ||||
|   bool did_change = false; | ||||
|   bool new_state = data->mode != MODE_STANDBY && data->mode != MODE_WAIT; | ||||
|  | ||||
|   if (new_state != this->state) { | ||||
|     this->state = new_state; | ||||
|     did_change = true; | ||||
|   } | ||||
|  | ||||
|   if (data->fan_step != this->speed) { | ||||
|     this->speed = data->fan_step; | ||||
|     did_change = true; | ||||
|   } | ||||
|  | ||||
|   if (did_change) { | ||||
|     this->publish_state(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Attempts to update the fan device from the last received BedjetStatusPacket. | ||||
|  * | ||||
|  * This will be called from #on_status() when the parent dispatches new status packets, | ||||
|  * and from #update() when the polling interval is triggered. | ||||
|  * | ||||
|  * @return `true` if the status has been applied; `false` if there is nothing to apply. | ||||
|  */ | ||||
| bool BedJetFan::update_status_() { | ||||
|   if (!this->parent_->is_connected()) | ||||
|     return false; | ||||
|   if (!this->parent_->has_status()) | ||||
|     return false; | ||||
|  | ||||
|   auto *status = this->parent_->get_status_packet(); | ||||
|  | ||||
|   if (status == nullptr) | ||||
|     return false; | ||||
|  | ||||
|   this->on_status(status); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void BedJetFan::update() { | ||||
|   ESP_LOGD(TAG, "[%s] update()", this->get_name().c_str()); | ||||
|   // TODO: if the hub component is already polling, do we also need to include polling? | ||||
|   //  We're already going to get on_status() at the hub's polling interval. | ||||
|   auto result = this->update_status_(); | ||||
|   ESP_LOGD(TAG, "[%s] update_status result=%s", this->get_name().c_str(), result ? "true" : "false"); | ||||
| } | ||||
|  | ||||
| /** Resets states to defaults. */ | ||||
| void BedJetFan::reset_state_() { | ||||
|   this->state = false; | ||||
|   this->publish_state(); | ||||
| } | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										40
									
								
								esphome/components/bedjet/fan/bedjet_fan.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								esphome/components/bedjet/fan/bedjet_fan.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/components/bedjet/bedjet_child.h" | ||||
| #include "esphome/components/bedjet/bedjet_codec.h" | ||||
| #include "esphome/components/bedjet/bedjet_hub.h" | ||||
| #include "esphome/components/fan/fan.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| class BedJetFan : public fan::Fan, public BedJetClient, public PollingComponent { | ||||
|  public: | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
|  | ||||
|   /* BedJetClient status update */ | ||||
|   void on_status(const BedjetStatusPacket *data) override; | ||||
|   void on_bedjet_state(bool is_ready) override{}; | ||||
|   std::string describe() override; | ||||
|  | ||||
|   fan::FanTraits get_traits() override { return fan::FanTraits(false, true, false, BEDJET_FAN_SPEED_COUNT); } | ||||
|  | ||||
|  protected: | ||||
|   void control(const fan::FanCall &call) override; | ||||
|  | ||||
|  private: | ||||
|   void reset_state_(); | ||||
|   bool update_status_(); | ||||
| }; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -13,6 +13,9 @@ void BinarySensorMap::loop() { | ||||
|     case BINARY_SENSOR_MAP_TYPE_GROUP: | ||||
|       this->process_group_(); | ||||
|       break; | ||||
|     case BINARY_SENSOR_MAP_TYPE_SUM: | ||||
|       this->process_sum_(); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -46,6 +49,34 @@ void BinarySensorMap::process_group_() { | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::process_sum_() { | ||||
|   float total_current_value = 0.0; | ||||
|   uint64_t mask = 0x00; | ||||
|   // check all binary_sensors for its state. when active add its value to total_current_value. | ||||
|   // create a bitmask for the binary_sensor status on all channels | ||||
|   for (size_t i = 0; i < this->channels_.size(); i++) { | ||||
|     auto bs = this->channels_[i]; | ||||
|     if (bs.binary_sensor->state) { | ||||
|       total_current_value += bs.sensor_value; | ||||
|       mask |= 1 << i; | ||||
|     } | ||||
|   } | ||||
|   // check if the sensor map was touched | ||||
|   if (mask != 0ULL) { | ||||
|     // did the bit_mask change or is it a new sensor touch | ||||
|     if (this->last_mask_ != mask) { | ||||
|       float publish_value = total_current_value; | ||||
|       ESP_LOGD(TAG, "'%s' - Publishing %.2f", this->name_.c_str(), publish_value); | ||||
|       this->publish_state(publish_value); | ||||
|     } | ||||
|   } else if (this->last_mask_ != 0ULL) { | ||||
|     // is this a new sensor release | ||||
|     ESP_LOGD(TAG, "'%s' - No binary sensor active, publishing 0", this->name_.c_str()); | ||||
|     this->publish_state(0.0); | ||||
|   } | ||||
|   this->last_mask_ = mask; | ||||
| } | ||||
|  | ||||
| void BinarySensorMap::add_channel(binary_sensor::BinarySensor *sensor, float value) { | ||||
|   BinarySensorMapChannel sensor_channel{ | ||||
|       .binary_sensor = sensor, | ||||
|   | ||||
| @@ -9,6 +9,7 @@ namespace binary_sensor_map { | ||||
|  | ||||
| enum BinarySensorMapType { | ||||
|   BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|   BINARY_SENSOR_MAP_TYPE_SUM, | ||||
| }; | ||||
|  | ||||
| struct BinarySensorMapChannel { | ||||
| @@ -50,8 +51,10 @@ class BinarySensorMap : public sensor::Sensor, public Component { | ||||
|   /** | ||||
|    * methods to process the types of binary_sensor_maps | ||||
|    * GROUP: process_group_() just map to a value | ||||
|    * ADD: process_add_() adds all the values | ||||
|    * */ | ||||
|   void process_group_(); | ||||
|   void process_sum_(); | ||||
| }; | ||||
|  | ||||
| }  // namespace binary_sensor_map | ||||
|   | ||||
| @@ -9,6 +9,7 @@ from esphome.const import ( | ||||
|     ICON_CHECK_CIRCLE_OUTLINE, | ||||
|     CONF_BINARY_SENSOR, | ||||
|     CONF_GROUP, | ||||
|     CONF_SUM, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["binary_sensor"] | ||||
| @@ -21,6 +22,7 @@ SensorMapType = binary_sensor_map_ns.enum("SensorMapType") | ||||
|  | ||||
| SENSOR_MAP_TYPES = { | ||||
|     CONF_GROUP: SensorMapType.BINARY_SENSOR_MAP_TYPE_GROUP, | ||||
|     CONF_SUM: SensorMapType.BINARY_SENSOR_MAP_TYPE_SUM, | ||||
| } | ||||
|  | ||||
| entry = { | ||||
| @@ -41,6 +43,17 @@ CONFIG_SCHEMA = cv.typed_schema( | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         CONF_SUM: sensor.sensor_schema( | ||||
|             BinarySensorMap, | ||||
|             icon=ICON_CHECK_CIRCLE_OUTLINE, | ||||
|             accuracy_decimals=0, | ||||
|         ).extend( | ||||
|             { | ||||
|                 cv.Required(CONF_CHANNELS): cv.All( | ||||
|                     cv.ensure_list(entry), cv.Length(min=1) | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|     }, | ||||
|     lower=True, | ||||
| ) | ||||
|   | ||||
							
								
								
									
										1
									
								
								esphome/components/bl0942/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bl0942/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@dbuezas"] | ||||
							
								
								
									
										121
									
								
								esphome/components/bl0942/bl0942.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/components/bl0942/bl0942.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,121 @@ | ||||
| #include "bl0942.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0942 { | ||||
|  | ||||
| static const char *const TAG = "bl0942"; | ||||
|  | ||||
| static const uint8_t BL0942_READ_COMMAND = 0x58; | ||||
| static const uint8_t BL0942_FULL_PACKET = 0xAA; | ||||
| static const uint8_t BL0942_PACKET_HEADER = 0x55; | ||||
|  | ||||
| static const uint8_t BL0942_WRITE_COMMAND = 0xA8; | ||||
| static const uint8_t BL0942_REG_I_FAST_RMS_CTRL = 0x10; | ||||
| static const uint8_t BL0942_REG_MODE = 0x18; | ||||
| static const uint8_t BL0942_REG_SOFT_RESET = 0x19; | ||||
| static const uint8_t BL0942_REG_USR_WRPROT = 0x1A; | ||||
| static const uint8_t BL0942_REG_TPS_CTRL = 0x1B; | ||||
|  | ||||
| // TODO: Confirm insialisation works as intended | ||||
| const uint8_t BL0942_INIT[5][6] = { | ||||
|     // Reset to default | ||||
|     {BL0942_WRITE_COMMAND, BL0942_REG_SOFT_RESET, 0x5A, 0x5A, 0x5A, 0x38}, | ||||
|     // Enable User Operation Write | ||||
|     {BL0942_WRITE_COMMAND, BL0942_REG_USR_WRPROT, 0x55, 0x00, 0x00, 0xF0}, | ||||
|     // 0x0100 = CF_UNABLE energy pulse, AC_FREQ_SEL 50Hz, RMS_UPDATE_SEL 800mS | ||||
|     {BL0942_WRITE_COMMAND, BL0942_REG_MODE, 0x00, 0x10, 0x00, 0x37}, | ||||
|     // 0x47FF = Over-current and leakage alarm on, Automatic temperature measurement, Interval 100mS | ||||
|     {BL0942_WRITE_COMMAND, BL0942_REG_TPS_CTRL, 0xFF, 0x47, 0x00, 0xFE}, | ||||
|     // 0x181C = Half cycle, Fast RMS threshold 6172 | ||||
|     {BL0942_WRITE_COMMAND, BL0942_REG_I_FAST_RMS_CTRL, 0x1C, 0x18, 0x00, 0x1B}}; | ||||
|  | ||||
| void BL0942::loop() { | ||||
|   DataPacket buffer; | ||||
|   if (!this->available()) { | ||||
|     return; | ||||
|   } | ||||
|   if (read_array((uint8_t *) &buffer, sizeof(buffer))) { | ||||
|     if (validate_checksum(&buffer)) { | ||||
|       received_package_(&buffer); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Junk on wire. Throwing away partial message"); | ||||
|     while (read() >= 0) | ||||
|       ; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool BL0942::validate_checksum(DataPacket *data) { | ||||
|   uint8_t checksum = BL0942_READ_COMMAND; | ||||
|   // Whole package but checksum | ||||
|   uint8_t *raw = (uint8_t *) data; | ||||
|   for (uint32_t i = 0; i < sizeof(*data) - 1; i++) { | ||||
|     checksum += raw[i]; | ||||
|   } | ||||
|   checksum ^= 0xFF; | ||||
|   if (checksum != data->checksum) { | ||||
|     ESP_LOGW(TAG, "BL0942 invalid checksum! 0x%02X != 0x%02X", checksum, data->checksum); | ||||
|   } | ||||
|   return checksum == data->checksum; | ||||
| } | ||||
|  | ||||
| void BL0942::update() { | ||||
|   this->flush(); | ||||
|   this->write_byte(BL0942_READ_COMMAND); | ||||
|   this->write_byte(BL0942_FULL_PACKET); | ||||
| } | ||||
|  | ||||
| void BL0942::setup() { | ||||
|   for (auto *i : BL0942_INIT) { | ||||
|     this->write_array(i, 6); | ||||
|     delay(1); | ||||
|   } | ||||
|   this->flush(); | ||||
| } | ||||
|  | ||||
| void BL0942::received_package_(DataPacket *data) { | ||||
|   // Bad header | ||||
|   if (data->frame_header != BL0942_PACKET_HEADER) { | ||||
|     ESP_LOGI(TAG, "Invalid data. Header mismatch: %d", data->frame_header); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   float v_rms = (uint24_t) data->v_rms / voltage_reference_; | ||||
|   float i_rms = (uint24_t) data->i_rms / current_reference_; | ||||
|   float watt = (int24_t) data->watt / power_reference_; | ||||
|   uint32_t cf_cnt = (uint24_t) data->cf_cnt; | ||||
|   float total_energy_consumption = cf_cnt / energy_reference_; | ||||
|   float frequency = 1000000.0f / data->frequency; | ||||
|  | ||||
|   if (voltage_sensor_ != nullptr) { | ||||
|     voltage_sensor_->publish_state(v_rms); | ||||
|   } | ||||
|   if (current_sensor_ != nullptr) { | ||||
|     current_sensor_->publish_state(i_rms); | ||||
|   } | ||||
|   if (power_sensor_ != nullptr) { | ||||
|     power_sensor_->publish_state(watt); | ||||
|   } | ||||
|   if (energy_sensor_ != nullptr) { | ||||
|     energy_sensor_->publish_state(total_energy_consumption); | ||||
|   } | ||||
|   if (frequency_sensor_ != nullptr) { | ||||
|     frequency_sensor_->publish_state(frequency); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV(TAG, "BL0942: U %fV, I %fA, P %fW, Cnt %d, ∫P %fkWh, frequency %f°Hz, status 0x%08X", v_rms, i_rms, watt, | ||||
|            cf_cnt, total_energy_consumption, frequency, data->status); | ||||
| } | ||||
|  | ||||
| void BL0942::dump_config() {  // NOLINT(readability-function-cognitive-complexity) | ||||
|   ESP_LOGCONFIG(TAG, "BL0942:"); | ||||
|   LOG_SENSOR("", "Voltage", this->voltage_sensor_); | ||||
|   LOG_SENSOR("", "Current", this->current_sensor_); | ||||
|   LOG_SENSOR("", "Power", this->power_sensor_); | ||||
|   LOG_SENSOR("", "Energy", this->energy_sensor_); | ||||
|   LOG_SENSOR("", "frequency", this->frequency_sensor_); | ||||
| } | ||||
|  | ||||
| }  // namespace bl0942 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										68
									
								
								esphome/components/bl0942/bl0942.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								esphome/components/bl0942/bl0942.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,68 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/datatypes.h" | ||||
| #include "esphome/components/uart/uart.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bl0942 { | ||||
|  | ||||
| static const float BL0942_PREF = 596;              // taken from tasmota | ||||
| static const float BL0942_UREF = 15873.35944299;   // should be 73989/1.218 | ||||
| static const float BL0942_IREF = 251213.46469622;  // 305978/1.218 | ||||
| static const float BL0942_EREF = 3304.61127328;    // Measured | ||||
|  | ||||
| struct DataPacket { | ||||
|   uint8_t frame_header; | ||||
|   uint24_le_t i_rms; | ||||
|   uint24_le_t v_rms; | ||||
|   uint24_le_t i_fast_rms; | ||||
|   int24_le_t watt; | ||||
|   uint24_le_t cf_cnt; | ||||
|   uint16_le_t frequency; | ||||
|   uint8_t reserved1; | ||||
|   uint8_t status; | ||||
|   uint8_t reserved2; | ||||
|   uint8_t reserved3; | ||||
|   uint8_t checksum; | ||||
| } __attribute__((packed)); | ||||
|  | ||||
| class BL0942 : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; } | ||||
|   void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; } | ||||
|   void set_power_sensor(sensor::Sensor *power_sensor) { power_sensor_ = power_sensor; } | ||||
|   void set_energy_sensor(sensor::Sensor *energy_sensor) { energy_sensor_ = energy_sensor; } | ||||
|   void set_frequency_sensor(sensor::Sensor *frequency_sensor) { frequency_sensor_ = frequency_sensor; } | ||||
|  | ||||
|   void loop() override; | ||||
|  | ||||
|   void update() override; | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *voltage_sensor_; | ||||
|   sensor::Sensor *current_sensor_; | ||||
|   // NB This may be negative as the circuits is seemingly able to measure | ||||
|   // power in both directions | ||||
|   sensor::Sensor *power_sensor_; | ||||
|   sensor::Sensor *energy_sensor_; | ||||
|   sensor::Sensor *frequency_sensor_; | ||||
|  | ||||
|   // Divide by this to turn into Watt | ||||
|   float power_reference_ = BL0942_PREF; | ||||
|   // Divide by this to turn into Volt | ||||
|   float voltage_reference_ = BL0942_UREF; | ||||
|   // Divide by this to turn into Ampere | ||||
|   float current_reference_ = BL0942_IREF; | ||||
|   // Divide by this to turn into kWh | ||||
|   float energy_reference_ = BL0942_EREF; | ||||
|  | ||||
|   static bool validate_checksum(DataPacket *data); | ||||
|  | ||||
|   void received_package_(DataPacket *data); | ||||
| }; | ||||
| }  // namespace bl0942 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										93
									
								
								esphome/components/bl0942/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								esphome/components/bl0942/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,93 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, uart | ||||
| from esphome.const import ( | ||||
|     CONF_CURRENT, | ||||
|     CONF_ENERGY, | ||||
|     CONF_ID, | ||||
|     CONF_POWER, | ||||
|     CONF_VOLTAGE, | ||||
|     CONF_FREQUENCY, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_ENERGY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     DEVICE_CLASS_FREQUENCY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_KILOWATT_HOURS, | ||||
|     UNIT_VOLT, | ||||
|     UNIT_WATT, | ||||
|     UNIT_HERTZ, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
| bl0942_ns = cg.esphome_ns.namespace("bl0942") | ||||
| BL0942 = bl0942_ns.class_("BL0942", cg.PollingComponent, uart.UARTDevice) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BL0942), | ||||
|             cv.Optional(CONF_VOLTAGE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_VOLT, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_VOLTAGE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_CURRENT): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_AMPERE, | ||||
|                 accuracy_decimals=2, | ||||
|                 device_class=DEVICE_CLASS_CURRENT, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_POWER): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_WATT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_POWER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_ENERGY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_KILOWATT_HOURS, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_ENERGY, | ||||
|             ), | ||||
|             cv.Optional(CONF_FREQUENCY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HERTZ, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_FREQUENCY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(uart.UART_DEVICE_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await uart.register_uart_device(var, config) | ||||
|  | ||||
|     if CONF_VOLTAGE in config: | ||||
|         conf = config[CONF_VOLTAGE] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_voltage_sensor(sens)) | ||||
|     if CONF_CURRENT in config: | ||||
|         conf = config[CONF_CURRENT] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_current_sensor(sens)) | ||||
|     if CONF_POWER in config: | ||||
|         conf = config[CONF_POWER] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_power_sensor(sens)) | ||||
|     if CONF_ENERGY in config: | ||||
|         conf = config[CONF_ENERGY] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_energy_sensor(sens)) | ||||
|     if CONF_FREQUENCY in config: | ||||
|         conf = config[CONF_FREQUENCY] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_frequency_sensor(sens)) | ||||
| @@ -1,7 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import switch, ble_client | ||||
| from esphome.const import CONF_ICON, CONF_ID, CONF_INVERTED, ICON_BLUETOOTH | ||||
| from esphome.const import ICON_BLUETOOTH | ||||
| from .. import ble_client_ns | ||||
|  | ||||
| BLEClientSwitch = ble_client_ns.class_( | ||||
| @@ -9,22 +9,13 @@ BLEClientSwitch = ble_client_ns.class_( | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     switch.SWITCH_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BLEClientSwitch), | ||||
|             cv.Optional(CONF_INVERTED): cv.invalid( | ||||
|                 "BLE client switches do not support inverted mode!" | ||||
|             ), | ||||
|             cv.Optional(CONF_ICON, default=ICON_BLUETOOTH): switch.icon, | ||||
|         } | ||||
|     ) | ||||
|     switch.switch_schema(BLEClientSwitch, icon=ICON_BLUETOOTH, block_inverted=True) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     var = await switch.new_switch(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await switch.register_switch(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|   | ||||
| @@ -12,41 +12,78 @@ namespace ble_rssi { | ||||
| class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component { | ||||
|  public: | ||||
|   void set_address(uint64_t address) { | ||||
|     this->by_address_ = true; | ||||
|     this->match_by_ = MATCH_BY_MAC_ADDRESS; | ||||
|     this->address_ = address; | ||||
|   } | ||||
|   void set_service_uuid16(uint16_t uuid) { | ||||
|     this->by_address_ = false; | ||||
|     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint16(uuid); | ||||
|   } | ||||
|   void set_service_uuid32(uint32_t uuid) { | ||||
|     this->by_address_ = false; | ||||
|     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_uint32(uuid); | ||||
|   } | ||||
|   void set_service_uuid128(uint8_t *uuid) { | ||||
|     this->by_address_ = false; | ||||
|     this->match_by_ = MATCH_BY_SERVICE_UUID; | ||||
|     this->uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); | ||||
|   } | ||||
|   void set_ibeacon_uuid(uint8_t *uuid) { | ||||
|     this->match_by_ = MATCH_BY_IBEACON_UUID; | ||||
|     this->ibeacon_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(uuid); | ||||
|   } | ||||
|   void set_ibeacon_major(uint16_t major) { | ||||
|     this->check_ibeacon_major_ = true; | ||||
|     this->ibeacon_major_ = major; | ||||
|   } | ||||
|   void set_ibeacon_minor(uint16_t minor) { | ||||
|     this->check_ibeacon_minor_ = true; | ||||
|     this->ibeacon_minor_ = minor; | ||||
|   } | ||||
|   void on_scan_end() override { | ||||
|     if (!this->found_) | ||||
|       this->publish_state(NAN); | ||||
|     this->found_ = false; | ||||
|   } | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { | ||||
|     if (this->by_address_) { | ||||
|       if (device.address_uint64() == this->address_) { | ||||
|         this->publish_state(device.get_rssi()); | ||||
|         this->found_ = true; | ||||
|         return true; | ||||
|       } | ||||
|     } else { | ||||
|       for (auto uuid : device.get_service_uuids()) { | ||||
|         if (this->uuid_ == uuid) { | ||||
|     switch (this->match_by_) { | ||||
|       case MATCH_BY_MAC_ADDRESS: | ||||
|         if (device.address_uint64() == this->address_) { | ||||
|           this->publish_state(device.get_rssi()); | ||||
|           this->found_ = true; | ||||
|           return true; | ||||
|         } | ||||
|       } | ||||
|         break; | ||||
|       case MATCH_BY_SERVICE_UUID: | ||||
|         for (auto uuid : device.get_service_uuids()) { | ||||
|           if (this->uuid_ == uuid) { | ||||
|             this->publish_state(device.get_rssi()); | ||||
|             this->found_ = true; | ||||
|             return true; | ||||
|           } | ||||
|         } | ||||
|         break; | ||||
|       case MATCH_BY_IBEACON_UUID: | ||||
|         if (!device.get_ibeacon().has_value()) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         auto ibeacon = device.get_ibeacon().value(); | ||||
|  | ||||
|         if (this->ibeacon_uuid_ != ibeacon.get_uuid()) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         if (this->check_ibeacon_major_ && this->ibeacon_major_ != ibeacon.get_major()) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         if (this->check_ibeacon_minor_ && this->ibeacon_minor_ != ibeacon.get_minor()) { | ||||
|           return false; | ||||
|         } | ||||
|  | ||||
|         this->publish_state(device.get_rssi()); | ||||
|         this->found_ = true; | ||||
|         return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| @@ -54,10 +91,20 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|  protected: | ||||
|   enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; | ||||
|   MatchType match_by_; | ||||
|  | ||||
|   bool found_{false}; | ||||
|   bool by_address_{false}; | ||||
|  | ||||
|   uint64_t address_; | ||||
|  | ||||
|   esp32_ble_tracker::ESPBTUUID uuid_; | ||||
|  | ||||
|   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; | ||||
|   uint16_t ibeacon_major_; | ||||
|   bool check_ibeacon_major_; | ||||
|   uint16_t ibeacon_minor_; | ||||
|   bool check_ibeacon_minor_; | ||||
| }; | ||||
|  | ||||
| }  // namespace ble_rssi | ||||
|   | ||||
| @@ -2,6 +2,9 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, esp32_ble_tracker | ||||
| from esphome.const import ( | ||||
|     CONF_IBEACON_MAJOR, | ||||
|     CONF_IBEACON_MINOR, | ||||
|     CONF_IBEACON_UUID, | ||||
|     CONF_SERVICE_UUID, | ||||
|     CONF_MAC_ADDRESS, | ||||
|     DEVICE_CLASS_SIGNAL_STRENGTH, | ||||
| @@ -16,6 +19,15 @@ BLERSSISensor = ble_rssi_ns.class_( | ||||
|     "BLERSSISensor", sensor.Sensor, cg.Component, esp32_ble_tracker.ESPBTDeviceListener | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _validate(config): | ||||
|     if CONF_IBEACON_MAJOR in config and CONF_IBEACON_UUID not in config: | ||||
|         raise cv.Invalid("iBeacon major identifier requires iBeacon UUID") | ||||
|     if CONF_IBEACON_MINOR in config and CONF_IBEACON_UUID not in config: | ||||
|         raise cv.Invalid("iBeacon minor identifier requires iBeacon UUID") | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     sensor.sensor_schema( | ||||
|         BLERSSISensor, | ||||
| @@ -28,11 +40,15 @@ CONFIG_SCHEMA = cv.All( | ||||
|         { | ||||
|             cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, | ||||
|             cv.Optional(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, | ||||
|             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, | ||||
|             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, | ||||
|             cv.Optional(CONF_IBEACON_UUID): cv.uuid, | ||||
|         } | ||||
|     ) | ||||
|     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||
|     .extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID), | ||||
|     cv.has_exactly_one_key(CONF_MAC_ADDRESS, CONF_SERVICE_UUID, CONF_IBEACON_UUID), | ||||
|     _validate, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -60,3 +76,13 @@ async def to_code(config): | ||||
|         elif len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid128_format): | ||||
|             uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) | ||||
|             cg.add(var.set_service_uuid128(uuid128)) | ||||
|  | ||||
|     if CONF_IBEACON_UUID in config: | ||||
|         ibeacon_uuid = esp32_ble_tracker.as_hex_array(str(config[CONF_IBEACON_UUID])) | ||||
|         cg.add(var.set_ibeacon_uuid(ibeacon_uuid)) | ||||
|  | ||||
|         if CONF_IBEACON_MAJOR in config: | ||||
|             cg.add(var.set_ibeacon_major(config[CONF_IBEACON_MAJOR])) | ||||
|  | ||||
|         if CONF_IBEACON_MINOR in config: | ||||
|             cg.add(var.set_ibeacon_minor(config[CONF_IBEACON_MINOR])) | ||||
|   | ||||
							
								
								
									
										27
									
								
								esphome/components/bluetooth_proxy/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/bluetooth_proxy/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| from esphome.components import esp32_ble_tracker | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| DEPENDENCIES = ["esp32", "esp32_ble_tracker"] | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|  | ||||
|  | ||||
| bluetooth_proxy_ns = cg.esphome_ns.namespace("bluetooth_proxy") | ||||
|  | ||||
| BluetoothProxy = bluetooth_proxy_ns.class_("BluetoothProxy", cg.Component) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(BluetoothProxy), | ||||
|     } | ||||
| ).extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     await esp32_ble_tracker.register_ble_device(var, config) | ||||
|  | ||||
|     cg.add_define("USE_BLUETOOTH_PROXY") | ||||
							
								
								
									
										58
									
								
								esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								esphome/components/bluetooth_proxy/bluetooth_proxy.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| #include "bluetooth_proxy.h" | ||||
|  | ||||
| #ifdef USE_API | ||||
| #include "esphome/components/api/api_pb2.h" | ||||
| #include "esphome/components/api/api_server.h" | ||||
| #endif  // USE_API | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
|  | ||||
| static const char *const TAG = "bluetooth_proxy"; | ||||
|  | ||||
| bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
|   ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), | ||||
|            device.get_rssi()); | ||||
|   this->send_api_packet_(device); | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | ||||
| #ifndef USE_API | ||||
|   return; | ||||
| #else | ||||
|   api::BluetoothLEAdvertisementResponse resp; | ||||
|   resp.address = device.address_uint64(); | ||||
|   if (!device.get_name().empty()) | ||||
|     resp.name = device.get_name(); | ||||
|   resp.rssi = device.get_rssi(); | ||||
|   for (auto uuid : device.get_service_uuids()) { | ||||
|     resp.service_uuids.push_back(uuid.to_string()); | ||||
|   } | ||||
|   for (auto &data : device.get_service_datas()) { | ||||
|     api::BluetoothServiceData service_data; | ||||
|     service_data.uuid = data.uuid.to_string(); | ||||
|     for (auto d : data.data) | ||||
|       service_data.data.push_back(d); | ||||
|     resp.service_data.push_back(service_data); | ||||
|   } | ||||
|   for (auto &data : device.get_manufacturer_datas()) { | ||||
|     api::BluetoothServiceData manufacturer_data; | ||||
|     manufacturer_data.uuid = data.uuid.to_string(); | ||||
|     for (auto d : data.data) | ||||
|       manufacturer_data.data.push_back(d); | ||||
|     resp.manufacturer_data.push_back(manufacturer_data); | ||||
|   } | ||||
|   api::global_api_server->send_bluetooth_le_advertisement(resp); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void BluetoothProxy::dump_config() { ESP_LOGCONFIG(TAG, "Bluetooth Proxy:"); } | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
							
								
								
									
										26
									
								
								esphome/components/bluetooth_proxy/bluetooth_proxy.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								esphome/components/bluetooth_proxy/bluetooth_proxy.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <map> | ||||
|  | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bluetooth_proxy { | ||||
|  | ||||
| class BluetoothProxy : public Component, public esp32_ble_tracker::ESPBTDeviceListener { | ||||
|  public: | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); | ||||
| }; | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif  // USE_ESP32 | ||||
| @@ -5,7 +5,6 @@ from esphome.const import ( | ||||
|     CONF_DEVICE_CLASS, | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_ICON, | ||||
|     CONF_ID, | ||||
|     CONF_SOURCE_ID, | ||||
| ) | ||||
| from esphome.core.entity_helpers import inherit_property_from | ||||
| @@ -15,12 +14,15 @@ from .. import copy_ns | ||||
| CopySwitch = copy_ns.class_("CopySwitch", switch.Switch, cg.Component) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(CopySwitch), | ||||
|         cv.Required(CONF_SOURCE_ID): cv.use_id(switch.Switch), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
| CONFIG_SCHEMA = ( | ||||
|     switch.switch_schema(CopySwitch) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_SOURCE_ID): cv.use_id(switch.Switch), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = cv.All( | ||||
|     inherit_property_from(CONF_ICON, CONF_SOURCE_ID), | ||||
| @@ -30,8 +32,7 @@ FINAL_VALIDATE_SCHEMA = cv.All( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await switch.register_switch(var, config) | ||||
|     var = await switch.new_switch(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     source = await cg.get_variable(config[CONF_SOURCE_ID]) | ||||
|   | ||||
| @@ -13,8 +13,9 @@ void CSE7766Component::loop() { | ||||
|     this->raw_data_index_ = 0; | ||||
|   } | ||||
|  | ||||
|   if (this->available() == 0) | ||||
|   if (this->available() == 0) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->last_transmission_ = now; | ||||
|   while (this->available() != 0) { | ||||
| @@ -22,6 +23,7 @@ void CSE7766Component::loop() { | ||||
|     if (!this->check_byte_()) { | ||||
|       this->raw_data_index_ = 0; | ||||
|       this->status_set_warning(); | ||||
|       continue; | ||||
|     } | ||||
|  | ||||
|     if (this->raw_data_index_ == 23) { | ||||
| @@ -51,8 +53,9 @@ bool CSE7766Component::check_byte_() { | ||||
|  | ||||
|   if (index == 23) { | ||||
|     uint8_t checksum = 0; | ||||
|     for (uint8_t i = 2; i < 23; i++) | ||||
|     for (uint8_t i = 2; i < 23; i++) { | ||||
|       checksum += this->raw_data_[i]; | ||||
|     } | ||||
|  | ||||
|     if (checksum != this->raw_data_[23]) { | ||||
|       ESP_LOGW(TAG, "Invalid checksum from CSE7766: 0x%02X != 0x%02X", checksum, this->raw_data_[23]); | ||||
| @@ -66,20 +69,34 @@ bool CSE7766Component::check_byte_() { | ||||
| void CSE7766Component::parse_data_() { | ||||
|   ESP_LOGVV(TAG, "CSE7766 Data: "); | ||||
|   for (uint8_t i = 0; i < 23; i++) { | ||||
|     ESP_LOGVV(TAG, "  i=%u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i, BYTE_TO_BINARY(this->raw_data_[i]), | ||||
|     ESP_LOGVV(TAG, "  %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]), | ||||
|               this->raw_data_[i]); | ||||
|   } | ||||
|  | ||||
|   uint8_t header1 = this->raw_data_[0]; | ||||
|   if (header1 == 0xAA) { | ||||
|     ESP_LOGW(TAG, "CSE7766 not calibrated!"); | ||||
|     ESP_LOGE(TAG, "CSE7766 not calibrated!"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if ((header1 & 0xF0) == 0xF0 && ((header1 >> 0) & 1) == 1) { | ||||
|     ESP_LOGW(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", header1); | ||||
|     ESP_LOGW(TAG, "  Coefficient storage area is abnormal."); | ||||
|     return; | ||||
|   bool power_cycle_exceeds_range = false; | ||||
|  | ||||
|   if ((header1 & 0xF0) == 0xF0) { | ||||
|     if (header1 & 0xD) { | ||||
|       ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1); | ||||
|       if (header1 & (1 << 3)) { | ||||
|         ESP_LOGE(TAG, "  Voltage cycle exceeds range."); | ||||
|       } | ||||
|       if (header1 & (1 << 2)) { | ||||
|         ESP_LOGE(TAG, "  Current cycle exceeds range."); | ||||
|       } | ||||
|       if (header1 & (1 << 0)) { | ||||
|         ESP_LOGE(TAG, "  Coefficient storage area is abnormal."); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     power_cycle_exceeds_range = header1 & (1 << 1); | ||||
|   } | ||||
|  | ||||
|   uint32_t voltage_calib = this->get_24_bit_uint_(2); | ||||
| @@ -92,46 +109,29 @@ void CSE7766Component::parse_data_() { | ||||
|   uint8_t adj = this->raw_data_[20]; | ||||
|   uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22]; | ||||
|  | ||||
|   bool power_ok = true; | ||||
|   bool voltage_ok = true; | ||||
|   bool current_ok = true; | ||||
|  | ||||
|   if (header1 > 0xF0) { | ||||
|     // ESP_LOGV(TAG, "CSE7766 reports abnormal hardware: (0x%02X)", byte); | ||||
|     if ((header1 >> 3) & 1) { | ||||
|       ESP_LOGV(TAG, "  Voltage cycle exceeds range."); | ||||
|       voltage_ok = false; | ||||
|     } | ||||
|     if ((header1 >> 2) & 1) { | ||||
|       ESP_LOGV(TAG, "  Current cycle exceeds range."); | ||||
|       current_ok = false; | ||||
|     } | ||||
|     if ((header1 >> 1) & 1) { | ||||
|       ESP_LOGV(TAG, "  Power cycle exceeds range."); | ||||
|       power_ok = false; | ||||
|     } | ||||
|     if ((header1 >> 0) & 1) { | ||||
|       ESP_LOGV(TAG, "  Coefficient storage area is abnormal."); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((adj & 0x40) == 0x40 && voltage_ok && current_ok) { | ||||
|   bool have_voltage = adj & 0x40; | ||||
|   if (have_voltage) { | ||||
|     // voltage cycle of serial port outputted is a complete cycle; | ||||
|     this->voltage_acc_ += voltage_calib / float(voltage_cycle); | ||||
|     this->voltage_counts_ += 1; | ||||
|   } | ||||
|  | ||||
|   float power = 0; | ||||
|   if ((adj & 0x10) == 0x10 && voltage_ok && current_ok && power_ok) { | ||||
|   bool have_power = adj & 0x10; | ||||
|   float power = 0.0f; | ||||
|  | ||||
|   if (have_power) { | ||||
|     // power cycle of serial port outputted is a complete cycle; | ||||
|     power = power_calib / float(power_cycle); | ||||
|     // According to the user manual, power cycle exceeding range means the measured power is 0 | ||||
|     if (!power_cycle_exceeds_range) { | ||||
|       power = power_calib / float(power_cycle); | ||||
|     } | ||||
|     this->power_acc_ += power; | ||||
|     this->power_counts_ += 1; | ||||
|  | ||||
|     uint32_t difference; | ||||
|     if (this->cf_pulses_last_ == 0) | ||||
|     if (this->cf_pulses_last_ == 0) { | ||||
|       this->cf_pulses_last_ = cf_pulses; | ||||
|     } | ||||
|  | ||||
|     if (cf_pulses < this->cf_pulses_last_) { | ||||
|       difference = cf_pulses + (0x10000 - this->cf_pulses_last_); | ||||
| @@ -139,41 +139,52 @@ void CSE7766Component::parse_data_() { | ||||
|       difference = cf_pulses - this->cf_pulses_last_; | ||||
|     } | ||||
|     this->cf_pulses_last_ = cf_pulses; | ||||
|     this->energy_total_ += difference * float(power_calib) / 1000000.0 / 3600.0; | ||||
|     this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f; | ||||
|     this->energy_total_counts_ += 1; | ||||
|   } | ||||
|  | ||||
|   if ((adj & 0x20) == 0x20 && current_ok && voltage_ok && power != 0.0) { | ||||
|   if (adj & 0x20) { | ||||
|     // indicates current cycle of serial port outputted is a complete cycle; | ||||
|     this->current_acc_ += current_calib / float(current_cycle); | ||||
|     float current = 0.0f; | ||||
|     if (have_voltage && !have_power) { | ||||
|       // Testing has shown that when we have voltage and current but not power, that means the power is 0. | ||||
|       // We report a power of 0, which in turn means we should report a current of 0. | ||||
|       this->power_counts_ += 1; | ||||
|     } else if (power != 0.0f) { | ||||
|       current = current_calib / float(current_cycle); | ||||
|     } | ||||
|     this->current_acc_ += current; | ||||
|     this->current_counts_ += 1; | ||||
|   } | ||||
| } | ||||
| void CSE7766Component::update() { | ||||
|   float voltage = this->voltage_counts_ > 0 ? this->voltage_acc_ / this->voltage_counts_ : 0.0f; | ||||
|   float current = this->current_counts_ > 0 ? this->current_acc_ / this->current_counts_ : 0.0f; | ||||
|   float power = this->power_counts_ > 0 ? this->power_acc_ / this->power_counts_ : 0.0f; | ||||
|   const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) { | ||||
|     if (counts != 0) { | ||||
|       const auto avg = acc / counts; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Got voltage_acc=%.2f current_acc=%.2f power_acc=%.2f", this->voltage_acc_, this->current_acc_, | ||||
|            this->power_acc_); | ||||
|   ESP_LOGV(TAG, "Got voltage_counts=%d current_counts=%d power_counts=%d", this->voltage_counts_, this->current_counts_, | ||||
|            this->power_counts_); | ||||
|   ESP_LOGD(TAG, "Got voltage=%.1fV current=%.1fA power=%.1fW", voltage, current, power); | ||||
|       ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%d %s=%.1f", name, acc, name, counts, name, avg); | ||||
|  | ||||
|   if (this->voltage_sensor_ != nullptr) | ||||
|     this->voltage_sensor_->publish_state(voltage); | ||||
|   if (this->current_sensor_ != nullptr) | ||||
|     this->current_sensor_->publish_state(current); | ||||
|   if (this->power_sensor_ != nullptr) | ||||
|     this->power_sensor_->publish_state(power); | ||||
|   if (this->energy_sensor_ != nullptr) | ||||
|     this->energy_sensor_->publish_state(this->energy_total_); | ||||
|       if (sensor != nullptr) { | ||||
|         sensor->publish_state(avg); | ||||
|       } | ||||
|  | ||||
|   this->voltage_acc_ = 0.0f; | ||||
|   this->current_acc_ = 0.0f; | ||||
|   this->power_acc_ = 0.0f; | ||||
|   this->voltage_counts_ = 0; | ||||
|   this->power_counts_ = 0; | ||||
|   this->current_counts_ = 0; | ||||
|       acc = 0.0f; | ||||
|       counts = 0; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_); | ||||
|   publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_); | ||||
|   publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_); | ||||
|  | ||||
|   if (this->energy_total_counts_ != 0) { | ||||
|     ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%d", this->energy_total_, this->energy_total_counts_); | ||||
|  | ||||
|     if (this->energy_sensor_ != nullptr) { | ||||
|       this->energy_sensor_->publish_state(this->energy_total_); | ||||
|     } | ||||
|     this->energy_total_counts_ = 0; | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) { | ||||
|   | ||||
| @@ -39,6 +39,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice { | ||||
|   uint32_t voltage_counts_{0}; | ||||
|   uint32_t current_counts_{0}; | ||||
|   uint32_t power_counts_{0}; | ||||
|   // Setting this to 1 means it will always publish 0 once at startup | ||||
|   uint32_t energy_total_counts_{1}; | ||||
| }; | ||||
|  | ||||
| }  // namespace cse7766 | ||||
|   | ||||
| @@ -10,13 +10,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(CustomSwitchConstructor), | ||||
|         cv.Required(CONF_LAMBDA): cv.returning_lambda, | ||||
|         cv.Required(CONF_SWITCHES): cv.ensure_list( | ||||
|             switch.SWITCH_SCHEMA.extend( | ||||
|                 { | ||||
|                     cv.GenerateID(): cv.declare_id(switch.Switch), | ||||
|                 } | ||||
|             ) | ||||
|         ), | ||||
|         cv.Required(CONF_SWITCHES): cv.ensure_list(switch.switch_schema(switch.Switch)), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|   | ||||
| @@ -349,13 +349,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                     CONF_ICON: ICON_BLUETOOTH, | ||||
|                 }, | ||||
|             ], | ||||
|         ): [ | ||||
|             switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA).extend( | ||||
|                 { | ||||
|                     cv.GenerateID(): cv.declare_id(DemoSwitch), | ||||
|                 } | ||||
|             ) | ||||
|         ], | ||||
|         ): [switch.switch_schema(DemoSwitch).extend(cv.COMPONENT_SCHEMA)], | ||||
|         cv.Optional( | ||||
|             CONF_TEXT_SENSORS, | ||||
|             default=[ | ||||
| @@ -422,9 +416,8 @@ async def to_code(config): | ||||
|         await cg.register_component(var, conf) | ||||
|  | ||||
|     for conf in config[CONF_SWITCHES]: | ||||
|         var = cg.new_Pvariable(conf[CONF_ID]) | ||||
|         var = await switch.new_switch(conf) | ||||
|         await cg.register_component(var, conf) | ||||
|         await switch.register_switch(var, conf) | ||||
|  | ||||
|     for conf in config[CONF_TEXT_SENSORS]: | ||||
|         var = await text_sensor.new_text_sensor(conf) | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/dps310/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/dps310/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										189
									
								
								esphome/components/dps310/dps310.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								esphome/components/dps310/dps310.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | ||||
| #include "dps310.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace dps310 { | ||||
|  | ||||
| static const char *const TAG = "dps310"; | ||||
|  | ||||
| void DPS310Component::setup() { | ||||
|   uint8_t coef_data_raw[DPS310_NUM_COEF_REGS]; | ||||
|   auto timer = DPS310_INIT_TIMEOUT; | ||||
|   uint8_t reg = 0; | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "Setting up DPS310..."); | ||||
|   // first, reset the sensor | ||||
|   if (!this->write_byte(DPS310_REG_RESET, DPS310_CMD_RESET)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   delay(10); | ||||
|   // wait for the sensor and its coefficients to be ready | ||||
|   while (timer-- && (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY))) { | ||||
|     reg = this->read_byte(DPS310_REG_MEAS_CFG).value_or(0); | ||||
|     delay(5); | ||||
|   } | ||||
|  | ||||
|   if (!(reg & DPS310_BIT_SENSOR_RDY) || !(reg & DPS310_BIT_COEF_RDY)) {  // the flags were not set in time | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   // read device ID | ||||
|   if (!this->read_byte(DPS310_REG_PROD_REV_ID, &this->prod_rev_id_)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   // read in coefficients used to calculate the compensated pressure and temperature values | ||||
|   if (!this->read_bytes(DPS310_REG_COEF, coef_data_raw, DPS310_NUM_COEF_REGS)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   // read in coefficients source register, too -- we need this a few lines down | ||||
|   if (!this->read_byte(DPS310_REG_TMP_COEF_SRC, ®)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   // set up operational stuff | ||||
|   if (!this->write_byte(DPS310_REG_PRS_CFG, DPS310_VAL_PRS_CFG)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->write_byte(DPS310_REG_TMP_CFG, DPS310_VAL_TMP_CFG | (reg & DPS310_BIT_TMP_COEF_SRC))) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->write_byte(DPS310_REG_CFG, DPS310_VAL_REG_CFG)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->write_byte(DPS310_REG_MEAS_CFG, 0x07)) {  // enable background mode | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->c0_ =  // we only ever use c0/2, so just divide by 2 here to save time later | ||||
|       DPS310Component::twos_complement( | ||||
|           int16_t(((uint16_t) coef_data_raw[0] << 4) | (((uint16_t) coef_data_raw[1] >> 4) & 0x0F)), 12) / | ||||
|       2; | ||||
|  | ||||
|   this->c1_ = | ||||
|       DPS310Component::twos_complement(int16_t((((uint16_t) coef_data_raw[1] & 0x0F) << 8) | coef_data_raw[2]), 12); | ||||
|  | ||||
|   this->c00_ = ((uint32_t) coef_data_raw[3] << 12) | ((uint32_t) coef_data_raw[4] << 4) | | ||||
|                (((uint32_t) coef_data_raw[5] >> 4) & 0x0F); | ||||
|   this->c00_ = DPS310Component::twos_complement(c00_, 20); | ||||
|  | ||||
|   this->c10_ = | ||||
|       (((uint32_t) coef_data_raw[5] & 0x0F) << 16) | ((uint32_t) coef_data_raw[6] << 8) | (uint32_t) coef_data_raw[7]; | ||||
|   this->c10_ = DPS310Component::twos_complement(c10_, 20); | ||||
|  | ||||
|   this->c01_ = int16_t(((uint16_t) coef_data_raw[8] << 8) | (uint16_t) coef_data_raw[9]); | ||||
|   this->c11_ = int16_t(((uint16_t) coef_data_raw[10] << 8) | (uint16_t) coef_data_raw[11]); | ||||
|   this->c20_ = int16_t(((uint16_t) coef_data_raw[12] << 8) | (uint16_t) coef_data_raw[13]); | ||||
|   this->c21_ = int16_t(((uint16_t) coef_data_raw[14] << 8) | (uint16_t) coef_data_raw[15]); | ||||
|   this->c30_ = int16_t(((uint16_t) coef_data_raw[16] << 8) | (uint16_t) coef_data_raw[17]); | ||||
| } | ||||
|  | ||||
| void DPS310Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "DPS310:"); | ||||
|   ESP_LOGCONFIG(TAG, "  Product ID: %u", this->prod_rev_id_ & 0x0F); | ||||
|   ESP_LOGCONFIG(TAG, "  Revision ID: %u", (this->prod_rev_id_ >> 4) & 0x0F); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Communication with DPS310 failed!"); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||
| } | ||||
|  | ||||
| float DPS310Component::get_setup_priority() const { return setup_priority::DATA; } | ||||
|  | ||||
| void DPS310Component::update() { | ||||
|   if (!this->update_in_progress_) { | ||||
|     this->update_in_progress_ = true; | ||||
|     this->read_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DPS310Component::read_() { | ||||
|   uint8_t reg = 0; | ||||
|   if (!this->read_byte(DPS310_REG_MEAS_CFG, ®)) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if ((!this->got_pres_) && (reg & DPS310_BIT_PRS_RDY)) { | ||||
|     this->read_pressure_(); | ||||
|   } | ||||
|  | ||||
|   if ((!this->got_temp_) && (reg & DPS310_BIT_TMP_RDY)) { | ||||
|     this->read_temperature_(); | ||||
|   } | ||||
|  | ||||
|   if (this->got_pres_ && this->got_temp_) { | ||||
|     this->calculate_values_(this->raw_temperature_, this->raw_pressure_); | ||||
|     this->got_pres_ = false; | ||||
|     this->got_temp_ = false; | ||||
|     this->update_in_progress_ = false; | ||||
|     this->status_clear_warning(); | ||||
|   } else { | ||||
|     auto f = std::bind(&DPS310Component::read_, this); | ||||
|     this->set_timeout("dps310", 10, f); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DPS310Component::read_pressure_() { | ||||
|   uint8_t bytes[3]; | ||||
|   if (!this->read_bytes(DPS310_REG_PRS_B2, bytes, 3)) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   this->got_pres_ = true; | ||||
|   this->raw_pressure_ = DPS310Component::twos_complement( | ||||
|       int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24); | ||||
| } | ||||
|  | ||||
| void DPS310Component::read_temperature_() { | ||||
|   uint8_t bytes[3]; | ||||
|   if (!this->read_bytes(DPS310_REG_TMP_B2, bytes, 3)) { | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|   this->got_temp_ = true; | ||||
|   this->raw_temperature_ = DPS310Component::twos_complement( | ||||
|       int32_t((uint32_t(bytes[0]) << 16) | (uint32_t(bytes[1]) << 8) | (uint32_t(bytes[2]))), 24); | ||||
| } | ||||
|  | ||||
| // Calculations are taken from the datasheet which can be found here: | ||||
| // https://www.infineon.com/dgdl/Infineon-DPS310-DataSheet-v01_02-EN.pdf?fileId=5546d462576f34750157750826c42242 | ||||
| // Sections "How to Calculate Compensated Pressure Values" and "How to Calculate Compensated Temperature Values" | ||||
| // Variable names below match variable names from the datasheet but lowercased | ||||
| void DPS310Component::calculate_values_(int32_t raw_temperature, int32_t raw_pressure) { | ||||
|   const float t_raw_sc = (float) raw_temperature / DPS310_SCALE_FACTOR; | ||||
|   const float p_raw_sc = (float) raw_pressure / DPS310_SCALE_FACTOR; | ||||
|  | ||||
|   const float temperature = t_raw_sc * this->c1_ + this->c0_;  // c0/2 done earlier! | ||||
|  | ||||
|   const float pressure = (this->c00_ + p_raw_sc * (this->c10_ + p_raw_sc * (this->c20_ + p_raw_sc * this->c30_)) + | ||||
|                           t_raw_sc * this->c01_ + t_raw_sc * p_raw_sc * (this->c11_ + p_raw_sc * this->c21_)) / | ||||
|                          100;  // divide by 100 for hPa | ||||
|  | ||||
|   if (this->temperature_sensor_ != nullptr) { | ||||
|     this->temperature_sensor_->publish_state(temperature); | ||||
|   } | ||||
|   if (this->pressure_sensor_ != nullptr) { | ||||
|     this->pressure_sensor_->publish_state(pressure); | ||||
|   } | ||||
| } | ||||
|  | ||||
| int32_t DPS310Component::twos_complement(int32_t val, uint8_t bits) { | ||||
|   if (val & ((uint32_t) 1 << (bits - 1))) { | ||||
|     val -= (uint32_t) 1 << bits; | ||||
|   } | ||||
|   return val; | ||||
| } | ||||
|  | ||||
| }  // namespace dps310 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										65
									
								
								esphome/components/dps310/dps310.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								esphome/components/dps310/dps310.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace dps310 { | ||||
|  | ||||
| static const uint8_t DPS310_REG_PRS_B2 = 0x00;        // Highest byte of pressure data | ||||
| static const uint8_t DPS310_REG_TMP_B2 = 0x03;        // Highest byte of temperature data | ||||
| static const uint8_t DPS310_REG_PRS_CFG = 0x06;       // Pressure configuration | ||||
| static const uint8_t DPS310_REG_TMP_CFG = 0x07;       // Temperature configuration | ||||
| static const uint8_t DPS310_REG_MEAS_CFG = 0x08;      // Sensor configuration | ||||
| static const uint8_t DPS310_REG_CFG = 0x09;           // Interrupt/FIFO configuration | ||||
| static const uint8_t DPS310_REG_RESET = 0x0C;         // Soft reset | ||||
| static const uint8_t DPS310_REG_PROD_REV_ID = 0x0D;   // Register that contains the part ID | ||||
| static const uint8_t DPS310_REG_COEF = 0x10;          // Top of calibration coefficient data space | ||||
| static const uint8_t DPS310_REG_TMP_COEF_SRC = 0x28;  // Temperature calibration src | ||||
|  | ||||
| static const uint8_t DPS310_BIT_PRS_RDY = 0x10;       // Pressure measurement is ready | ||||
| static const uint8_t DPS310_BIT_TMP_RDY = 0x20;       // Temperature measurement is ready | ||||
| static const uint8_t DPS310_BIT_SENSOR_RDY = 0x40;    // Sensor initialization complete when bit is set | ||||
| static const uint8_t DPS310_BIT_COEF_RDY = 0x80;      // Coefficients are available when bit is set | ||||
| static const uint8_t DPS310_BIT_TMP_COEF_SRC = 0x80;  // Temperature measurement source (0 = ASIC, 1 = MEMS element) | ||||
| static const uint8_t DPS310_BIT_REQ_PRES = 0x01;      // Set this bit to request pressure reading | ||||
| static const uint8_t DPS310_BIT_REQ_TEMP = 0x02;      // Set this bit to request temperature reading | ||||
|  | ||||
| static const uint8_t DPS310_CMD_RESET = 0x89;  // What to write to reset the device | ||||
|  | ||||
| static const uint8_t DPS310_VAL_PRS_CFG = 0x01;  // Value written to DPS310_REG_PRS_CFG at startup | ||||
| static const uint8_t DPS310_VAL_TMP_CFG = 0x01;  // Value written to DPS310_REG_TMP_CFG at startup | ||||
| static const uint8_t DPS310_VAL_REG_CFG = 0x00;  // Value written to DPS310_REG_CFG at startup | ||||
|  | ||||
| static const uint8_t DPS310_INIT_TIMEOUT = 20;       // How long to wait for DPS310 start-up to complete | ||||
| static const uint8_t DPS310_NUM_COEF_REGS = 18;      // Number of coefficients we need to read from the device | ||||
| static const int32_t DPS310_SCALE_FACTOR = 1572864;  // Measurement compensation scale factor | ||||
|  | ||||
| class DPS310Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void update() override; | ||||
|  | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; } | ||||
|   void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; } | ||||
|  | ||||
|  protected: | ||||
|   void read_(); | ||||
|   void read_pressure_(); | ||||
|   void read_temperature_(); | ||||
|   void calculate_values_(int32_t raw_temperature, int32_t raw_pressure); | ||||
|   static int32_t twos_complement(int32_t val, uint8_t bits); | ||||
|  | ||||
|   sensor::Sensor *temperature_sensor_; | ||||
|   sensor::Sensor *pressure_sensor_; | ||||
|   int32_t raw_pressure_, raw_temperature_, c00_, c10_; | ||||
|   int16_t c0_, c1_, c01_, c11_, c20_, c21_, c30_; | ||||
|   uint8_t prod_rev_id_; | ||||
|   bool got_pres_, got_temp_, update_in_progress_; | ||||
| }; | ||||
|  | ||||
| }  // namespace dps310 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										62
									
								
								esphome/components/dps310/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								esphome/components/dps310/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,62 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_PRESSURE, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     ICON_GAUGE, | ||||
|     ICON_THERMOMETER, | ||||
|     UNIT_HECTOPASCAL, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@kbx81"] | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| dps310_ns = cg.esphome_ns.namespace("dps310") | ||||
| DPS310Component = dps310_ns.class_( | ||||
|     "DPS310Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(DPS310Component), | ||||
|             cv.Required(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 icon=ICON_THERMOMETER, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Required(CONF_PRESSURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, | ||||
|                 icon=ICON_GAUGE, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_PRESSURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x77)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if CONF_TEMPERATURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||
|         cg.add(var.set_temperature_sensor(sens)) | ||||
|  | ||||
|     if CONF_PRESSURE in config: | ||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||
|         cg.add(var.set_pressure_sensor(sens)) | ||||
| @@ -141,7 +141,7 @@ class ESP32Preferences : public ESPPreferences { | ||||
|     ESP_LOGD(TAG, "Saving %d preferences to flash: %d cached, %d written, %d failed", cached + written + failed, cached, | ||||
|              written, failed); | ||||
|     if (failed > 0) { | ||||
|       ESP_LOGD(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), | ||||
|       ESP_LOGE(TAG, "Error saving %d preferences to flash. Last error=%s for key=%s", failed, esp_err_to_name(last_err), | ||||
|                last_key.c_str()); | ||||
|     } | ||||
|  | ||||
| @@ -170,6 +170,17 @@ class ESP32Preferences : public ESPPreferences { | ||||
|     } | ||||
|     return to_save.data != stored_data.data; | ||||
|   } | ||||
|  | ||||
|   bool reset() override { | ||||
|     ESP_LOGD(TAG, "Cleaning up preferences in flash..."); | ||||
|     s_pending_save.clear(); | ||||
|  | ||||
|     nvs_flash_deinit(); | ||||
|     nvs_flash_erase(); | ||||
|     // Make the handle invalid to prevent any saves until restart | ||||
|     nvs_handle = 0; | ||||
|     return true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| void setup_preferences() { | ||||
|   | ||||
| @@ -23,6 +23,8 @@ CONF_ESP32_BLE_ID = "esp32_ble_id" | ||||
| CONF_SCAN_PARAMETERS = "scan_parameters" | ||||
| CONF_WINDOW = "window" | ||||
| CONF_ACTIVE = "active" | ||||
| CONF_CONTINUOUS = "continuous" | ||||
| CONF_ON_SCAN_END = "on_scan_end" | ||||
| esp32_ble_tracker_ns = cg.esphome_ns.namespace("esp32_ble_tracker") | ||||
| ESP32BLETracker = esp32_ble_tracker_ns.class_("ESP32BLETracker", cg.Component) | ||||
| ESPBTClient = esp32_ble_tracker_ns.class_("ESPBTClient") | ||||
| @@ -42,6 +44,16 @@ BLEManufacturerDataAdvertiseTrigger = esp32_ble_tracker_ns.class_( | ||||
|     "BLEManufacturerDataAdvertiseTrigger", | ||||
|     automation.Trigger.template(adv_data_t_const_ref), | ||||
| ) | ||||
| BLEEndOfScanTrigger = esp32_ble_tracker_ns.class_( | ||||
|     "BLEEndOfScanTrigger", automation.Trigger.template() | ||||
| ) | ||||
| # Actions | ||||
| ESP32BLEStartScanAction = esp32_ble_tracker_ns.class_( | ||||
|     "ESP32BLEStartScanAction", automation.Action | ||||
| ) | ||||
| ESP32BLEStopScanAction = esp32_ble_tracker_ns.class_( | ||||
|     "ESP32BLEStopScanAction", automation.Action | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate_scan_parameters(config): | ||||
| @@ -138,6 +150,7 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                         CONF_WINDOW, default="30ms" | ||||
|                     ): cv.positive_time_period_milliseconds, | ||||
|                     cv.Optional(CONF_ACTIVE, default=True): cv.boolean, | ||||
|                     cv.Optional(CONF_CONTINUOUS, default=True): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
|             validate_scan_parameters, | ||||
| @@ -168,6 +181,9 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|                 cv.Required(CONF_MANUFACTURER_ID): bt_uuid, | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_SCAN_END): automation.validate_automation( | ||||
|             {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(BLEEndOfScanTrigger)} | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -186,6 +202,7 @@ async def to_code(config): | ||||
|     cg.add(var.set_scan_interval(int(params[CONF_INTERVAL].total_milliseconds / 0.625))) | ||||
|     cg.add(var.set_scan_window(int(params[CONF_WINDOW].total_milliseconds / 0.625))) | ||||
|     cg.add(var.set_scan_active(params[CONF_ACTIVE])) | ||||
|     cg.add(var.set_scan_continuous(params[CONF_CONTINUOUS])) | ||||
|     for conf in config.get(CONF_ON_BLE_ADVERTISE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         if CONF_MAC_ADDRESS in conf: | ||||
| @@ -215,10 +232,59 @@ async def to_code(config): | ||||
|         if CONF_MAC_ADDRESS in conf: | ||||
|             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) | ||||
|         await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf) | ||||
|     for conf in config.get(CONF_ON_SCAN_END, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|         add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True) | ||||
|  | ||||
|     cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts | ||||
|  | ||||
|  | ||||
| ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.GenerateID(): cv.use_id(ESP32BLETracker), | ||||
|         cv.Optional(CONF_CONTINUOUS, default=False): cv.templatable(cv.boolean), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "esp32_ble_tracker.start_scan", | ||||
|     ESP32BLEStartScanAction, | ||||
|     ESP32_BLE_START_SCAN_ACTION_SCHEMA, | ||||
| ) | ||||
| async def esp32_ble_tracker_start_scan_action_to_code( | ||||
|     config, action_id, template_arg, args | ||||
| ): | ||||
|     paren = await cg.get_variable(config[CONF_ID]) | ||||
|     var = cg.new_Pvariable(action_id, template_arg, paren) | ||||
|     cg.add(var.set_continuous(config[CONF_CONTINUOUS])) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| ESP32_BLE_STOP_SCAN_ACTION_SCHEMA = automation.maybe_simple_id( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.use_id(ESP32BLETracker), | ||||
|         } | ||||
|     ) | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "esp32_ble_tracker.stop_scan", | ||||
|     ESP32BLEStopScanAction, | ||||
|     ESP32_BLE_STOP_SCAN_ACTION_SCHEMA, | ||||
| ) | ||||
| async def esp32_ble_tracker_stop_scan_action_to_code( | ||||
|     config, action_id, template_arg, args | ||||
| ): | ||||
|     var = cg.new_Pvariable(action_id, template_arg) | ||||
|     await cg.register_parented(var, config[CONF_ID]) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def register_ble_device(var, config): | ||||
|     paren = await cg.get_variable(config[CONF_ESP32_BLE_ID]) | ||||
|   | ||||
| @@ -76,6 +76,32 @@ class BLEManufacturerDataAdvertiseTrigger : public Trigger<const adv_data_t &>, | ||||
|   ESPBTUUID uuid_; | ||||
| }; | ||||
|  | ||||
| class BLEEndOfScanTrigger : public Trigger<>, public ESPBTDeviceListener { | ||||
|  public: | ||||
|   explicit BLEEndOfScanTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } | ||||
|  | ||||
|   bool parse_device(const ESPBTDevice &device) override { return false; } | ||||
|   void on_scan_end() override { this->trigger(); } | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class ESP32BLEStartScanAction : public Action<Ts...> { | ||||
|  public: | ||||
|   ESP32BLEStartScanAction(ESP32BLETracker *parent) : parent_(parent) {} | ||||
|   TEMPLATABLE_VALUE(bool, continuous) | ||||
|   void play(Ts... x) override { | ||||
|     this->parent_->set_scan_continuous(this->continuous_.value(x...)); | ||||
|     this->parent_->start_scan(); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   ESP32BLETracker *parent_; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class ESP32BLEStopScanAction : public Action<Ts...>, public Parented<ESP32BLETracker> { | ||||
|  public: | ||||
|   void play(Ts... x) override { this->parent_->stop_scan(); } | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble_tracker | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,11 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esp32_ble_tracker.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <nvs_flash.h> | ||||
| #include <freertos/FreeRTOSConfig.h> | ||||
| @@ -15,6 +16,10 @@ | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_bt_defs.h> | ||||
|  | ||||
| #ifdef USE_OTA | ||||
| #include "esphome/components/ota/ota_component.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #include <esp32-hal-bt.h> | ||||
| #endif | ||||
| @@ -46,13 +51,23 @@ void ESP32BLETracker::setup() { | ||||
|   global_esp32_ble_tracker = this; | ||||
|   this->scan_result_lock_ = xSemaphoreCreateMutex(); | ||||
|   this->scan_end_lock_ = xSemaphoreCreateMutex(); | ||||
|  | ||||
|   this->scanner_idle_ = true; | ||||
|   if (!ESP32BLETracker::ble_setup()) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   global_esp32_ble_tracker->start_scan_(true); | ||||
| #ifdef USE_OTA | ||||
|   ota::global_ota_component->add_on_state_callback([this](ota::OTAState state, float progress, uint8_t error) { | ||||
|     if (state == ota::OTA_STARTED) { | ||||
|       this->stop_scan(); | ||||
|     } | ||||
|   }); | ||||
| #endif | ||||
|  | ||||
|   if (this->scan_continuous_) { | ||||
|     this->start_scan_(true); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::loop() { | ||||
| @@ -68,14 +83,25 @@ void ESP32BLETracker::loop() { | ||||
|     ble_event = this->ble_events_.pop(); | ||||
|   } | ||||
|  | ||||
|   if (this->scanner_idle_) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   bool connecting = false; | ||||
|   for (auto *client : this->clients_) { | ||||
|     if (client->state() == ClientState::CONNECTING || client->state() == ClientState::DISCOVERED) | ||||
|       connecting = true; | ||||
|   } | ||||
|  | ||||
|   if (!connecting && xSemaphoreTake(this->scan_end_lock_, 0L)) { | ||||
|     xSemaphoreGive(this->scan_end_lock_); | ||||
|     global_esp32_ble_tracker->start_scan_(false); | ||||
|     if (this->scan_continuous_) { | ||||
|       this->start_scan_(false); | ||||
|     } else if (xSemaphoreTake(this->scan_end_lock_, 0L) && !this->scanner_idle_) { | ||||
|       xSemaphoreGive(this->scan_end_lock_); | ||||
|       this->end_of_scan_(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { | ||||
| @@ -134,6 +160,22 @@ void ESP32BLETracker::loop() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::start_scan() { | ||||
|   if (xSemaphoreTake(this->scan_end_lock_, 0L)) { | ||||
|     xSemaphoreGive(this->scan_end_lock_); | ||||
|     this->start_scan_(true); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "Scan requested when a scan is already in progress. Ignoring."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::stop_scan() { | ||||
|   ESP_LOGD(TAG, "Stopping scan."); | ||||
|   this->scan_continuous_ = false; | ||||
|   esp_ble_gap_stop_scanning(); | ||||
|   this->cancel_timeout("scan"); | ||||
| } | ||||
|  | ||||
| bool ESP32BLETracker::ble_setup() { | ||||
|   // Initialize non-volatile storage for the bluetooth controller | ||||
|   esp_err_t err = nvs_flash_init(); | ||||
| @@ -225,6 +267,7 @@ void ESP32BLETracker::start_scan_(bool first) { | ||||
|       listener->on_scan_end(); | ||||
|   } | ||||
|   this->already_discovered_.clear(); | ||||
|   this->scanner_idle_ = false; | ||||
|   this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE; | ||||
|   this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC; | ||||
|   this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL; | ||||
| @@ -240,6 +283,22 @@ void ESP32BLETracker::start_scan_(bool first) { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::end_of_scan_() { | ||||
|   if (!xSemaphoreTake(this->scan_end_lock_, 0L)) { | ||||
|     ESP_LOGW(TAG, "Cannot clean up end of scan!"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "End of scan."); | ||||
|   this->scanner_idle_ = true; | ||||
|   this->already_discovered_.clear(); | ||||
|   xSemaphoreGive(this->scan_end_lock_); | ||||
|   this->cancel_timeout("scan"); | ||||
|  | ||||
|   for (auto *listener : this->listeners_) | ||||
|     listener->on_scan_end(); | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::register_client(ESPBTClient *client) { | ||||
|   client->app_id = ++this->app_id_; | ||||
|   this->clients_.push_back(client); | ||||
| @@ -253,21 +312,21 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga | ||||
| void ESP32BLETracker::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|       global_esp32_ble_tracker->gap_scan_result_(param->scan_rst); | ||||
|       this->gap_scan_result_(param->scan_rst); | ||||
|       break; | ||||
|     case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|       global_esp32_ble_tracker->gap_scan_set_param_complete_(param->scan_param_cmpl); | ||||
|       this->gap_scan_set_param_complete_(param->scan_param_cmpl); | ||||
|       break; | ||||
|     case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|       global_esp32_ble_tracker->gap_scan_start_complete_(param->scan_start_cmpl); | ||||
|       this->gap_scan_start_complete_(param->scan_start_cmpl); | ||||
|       break; | ||||
|     case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|       global_esp32_ble_tracker->gap_scan_stop_complete_(param->scan_stop_cmpl); | ||||
|       this->gap_scan_stop_complete_(param->scan_stop_cmpl); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
|   for (auto *client : global_esp32_ble_tracker->clients_) { | ||||
|   for (auto *client : this->clients_) { | ||||
|     client->gap_event_handler(event, param); | ||||
|   } | ||||
| } | ||||
| @@ -305,7 +364,7 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i | ||||
|  | ||||
| void ESP32BLETracker::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                                 esp_ble_gattc_cb_param_t *param) { | ||||
|   for (auto *client : global_esp32_ble_tracker->clients_) { | ||||
|   for (auto *client : this->clients_) { | ||||
|     client->gattc_event_handler(event, gattc_if, param); | ||||
|   } | ||||
| } | ||||
| @@ -719,7 +778,9 @@ void ESP32BLETracker::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "  Scan Interval: %.1f ms", this->scan_interval_ * 0.625f); | ||||
|   ESP_LOGCONFIG(TAG, "  Scan Window: %.1f ms", this->scan_window_ * 0.625f); | ||||
|   ESP_LOGCONFIG(TAG, "  Scan Type: %s", this->scan_active_ ? "ACTIVE" : "PASSIVE"); | ||||
|   ESP_LOGCONFIG(TAG, "  Continuous Scanning: %s", this->scan_continuous_ ? "True" : "False"); | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) { | ||||
|   const uint64_t address = device.address_uint64(); | ||||
|   for (auto &disc : this->already_discovered_) { | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "queue.h" | ||||
|  | ||||
| @@ -171,6 +172,7 @@ class ESP32BLETracker : public Component { | ||||
|   void set_scan_interval(uint32_t scan_interval) { scan_interval_ = scan_interval; } | ||||
|   void set_scan_window(uint32_t scan_window) { scan_window_ = scan_window; } | ||||
|   void set_scan_active(bool scan_active) { scan_active_ = scan_active; } | ||||
|   void set_scan_continuous(bool scan_continuous) { scan_continuous_ = scan_continuous; } | ||||
|  | ||||
|   /// Setup the FreeRTOS task and the Bluetooth stack. | ||||
|   void setup() override; | ||||
| @@ -188,11 +190,16 @@ class ESP32BLETracker : public Component { | ||||
|  | ||||
|   void print_bt_device_info(const ESPBTDevice &device); | ||||
|  | ||||
|   void start_scan(); | ||||
|   void stop_scan(); | ||||
|  | ||||
|  protected: | ||||
|   /// The FreeRTOS task managing the bluetooth interface. | ||||
|   static bool ble_setup(); | ||||
|   /// Start a single scan by setting up the parameters and doing some esp-idf calls. | ||||
|   void start_scan_(bool first); | ||||
|   /// Called when a scan ends | ||||
|   void end_of_scan_(); | ||||
|   /// Callback that will handle all GAP events and redistribute them to other callbacks. | ||||
|   static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); | ||||
|   void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); | ||||
| @@ -221,7 +228,9 @@ class ESP32BLETracker : public Component { | ||||
|   uint32_t scan_duration_; | ||||
|   uint32_t scan_interval_; | ||||
|   uint32_t scan_window_; | ||||
|   bool scan_continuous_; | ||||
|   bool scan_active_; | ||||
|   bool scanner_idle_; | ||||
|   SemaphoreHandle_t scan_result_lock_; | ||||
|   SemaphoreHandle_t scan_end_lock_; | ||||
|   size_t scan_result_index_{0}; | ||||
|   | ||||
| @@ -243,17 +243,34 @@ class ESP8266Preferences : public ESPPreferences { | ||||
|       } | ||||
|     } | ||||
|     if (erase_res != SPI_FLASH_RESULT_OK) { | ||||
|       ESP_LOGV(TAG, "Erase ESP8266 flash failed!"); | ||||
|       ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); | ||||
|       return false; | ||||
|     } | ||||
|     if (write_res != SPI_FLASH_RESULT_OK) { | ||||
|       ESP_LOGV(TAG, "Write ESP8266 flash failed!"); | ||||
|       ESP_LOGE(TAG, "Write ESP8266 flash failed!"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     s_flash_dirty = false; | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   bool reset() override { | ||||
|     ESP_LOGD(TAG, "Cleaning up preferences in flash..."); | ||||
|     SpiFlashOpResult erase_res; | ||||
|     { | ||||
|       InterruptLock lock; | ||||
|       erase_res = spi_flash_erase_sector(get_esp8266_flash_sector()); | ||||
|     } | ||||
|     if (erase_res != SPI_FLASH_RESULT_OK) { | ||||
|       ESP_LOGE(TAG, "Erase ESP8266 flash failed!"); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     // Protect flash from writing till restart | ||||
|     s_prevent_write = true; | ||||
|     return true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| void setup_preferences() { | ||||
|   | ||||
| @@ -31,6 +31,7 @@ EthernetType = ethernet_ns.enum("EthernetType") | ||||
| ETHERNET_TYPES = { | ||||
|     "LAN8720": EthernetType.ETHERNET_TYPE_LAN8720, | ||||
|     "TLK110": EthernetType.ETHERNET_TYPE_TLK110, | ||||
|     "IP101": EthernetType.ETHERNET_TYPE_IP101, | ||||
| } | ||||
|  | ||||
| eth_clock_mode_t = cg.global_ns.enum("eth_clock_mode_t") | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| #ifdef USE_ESP32_FRAMEWORK_ARDUINO | ||||
|  | ||||
| #include <eth_phy/phy_lan8720.h> | ||||
| #include <eth_phy/phy_ip101.h> | ||||
| #include <eth_phy/phy_tlk110.h> | ||||
| #include <lwip/dns.h> | ||||
|  | ||||
| @@ -33,6 +34,7 @@ EthernetComponent *global_eth_component;  // NOLINT(cppcoreguidelines-avoid-non- | ||||
|   } | ||||
|  | ||||
| EthernetComponent::EthernetComponent() { global_eth_component = this; } | ||||
|  | ||||
| void EthernetComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up Ethernet..."); | ||||
|  | ||||
| @@ -52,6 +54,10 @@ void EthernetComponent::setup() { | ||||
|       memcpy(&this->eth_config_, &phy_tlk110_default_ethernet_config, sizeof(eth_config_t)); | ||||
|       break; | ||||
|     } | ||||
|     case ETHERNET_TYPE_IP101: { | ||||
|       memcpy(&this->eth_config_, &phy_ip101_default_ethernet_config, sizeof(eth_config_t)); | ||||
|       break; | ||||
|     } | ||||
|     default: { | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
| @@ -76,6 +82,7 @@ void EthernetComponent::setup() { | ||||
|   err = esp_eth_enable(); | ||||
|   ESPHL_ERROR_CHECK(err, "ETH enable error"); | ||||
| } | ||||
|  | ||||
| void EthernetComponent::loop() { | ||||
|   const uint32_t now = millis(); | ||||
|  | ||||
| @@ -115,16 +122,39 @@ void EthernetComponent::loop() { | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void EthernetComponent::dump_config() { | ||||
|   std::string eth_type; | ||||
|   switch (this->type_) { | ||||
|     case ETHERNET_TYPE_LAN8720: | ||||
|       eth_type = "LAN8720"; | ||||
|       break; | ||||
|  | ||||
|     case ETHERNET_TYPE_TLK110: | ||||
|       eth_type = "TLK110"; | ||||
|       break; | ||||
|  | ||||
|     case ETHERNET_TYPE_IP101: | ||||
|       eth_type = "IP101"; | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       eth_type = "Unknown"; | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "Ethernet:"); | ||||
|   this->dump_connect_params_(); | ||||
|   LOG_PIN("  Power Pin: ", this->power_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  MDC Pin: %u", this->mdc_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  MDIO Pin: %u", this->mdio_pin_); | ||||
|   ESP_LOGCONFIG(TAG, "  Type: %s", this->type_ == ETHERNET_TYPE_LAN8720 ? "LAN8720" : "TLK110"); | ||||
|   ESP_LOGCONFIG(TAG, "  Type: %s", eth_type.c_str()); | ||||
| } | ||||
|  | ||||
| float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; } | ||||
|  | ||||
| bool EthernetComponent::can_proceed() { return this->is_connected(); } | ||||
|  | ||||
| network::IPAddress EthernetComponent::get_ip_address() { | ||||
|   tcpip_adapter_ip_info_t ip; | ||||
|   tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); | ||||
| @@ -213,17 +243,21 @@ void EthernetComponent::start_connect_() { | ||||
|   this->connect_begin_ = millis(); | ||||
|   this->status_set_warning(); | ||||
| } | ||||
|  | ||||
| void EthernetComponent::eth_phy_config_gpio() { | ||||
|   phy_rmii_configure_data_interface_pins(); | ||||
|   phy_rmii_smi_configure_pins(global_eth_component->mdc_pin_, global_eth_component->mdio_pin_); | ||||
| } | ||||
|  | ||||
| void EthernetComponent::eth_phy_power_enable(bool enable) { | ||||
|   global_eth_component->power_pin_->digital_write(enable); | ||||
|   // power up takes some time, datasheet says max 300µs | ||||
|   delay(1); | ||||
|   global_eth_component->orig_power_enable_fun_(enable); | ||||
| } | ||||
|  | ||||
| bool EthernetComponent::is_connected() { return this->state_ == EthernetComponentState::CONNECTED; } | ||||
|  | ||||
| void EthernetComponent::dump_connect_params_() { | ||||
|   tcpip_adapter_ip_info_t ip; | ||||
|   tcpip_adapter_get_ip_info(TCPIP_ADAPTER_IF_ETH, &ip); | ||||
| @@ -250,6 +284,7 @@ void EthernetComponent::dump_connect_params_() { | ||||
|   ESP_LOGCONFIG(TAG, "  Link Up: %s", YESNO(this->eth_config_.phy_check_link())); | ||||
|   ESP_LOGCONFIG(TAG, "  Link Speed: %u", this->eth_config_.phy_get_speed_mode() ? 100 : 10); | ||||
| } | ||||
|  | ||||
| void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; } | ||||
| void EthernetComponent::set_power_pin(GPIOPin *power_pin) { this->power_pin_ = power_pin; } | ||||
| void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; } | ||||
| @@ -257,12 +292,14 @@ void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_ | ||||
| void EthernetComponent::set_type(EthernetType type) { this->type_ = type; } | ||||
| void EthernetComponent::set_clk_mode(eth_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; } | ||||
| void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; } | ||||
|  | ||||
| std::string EthernetComponent::get_use_address() const { | ||||
|   if (this->use_address_.empty()) { | ||||
|     return App.get_name() + ".local"; | ||||
|   } | ||||
|   return this->use_address_; | ||||
| } | ||||
|  | ||||
| void EthernetComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } | ||||
|  | ||||
| }  // namespace ethernet | ||||
|   | ||||
| @@ -17,6 +17,7 @@ namespace ethernet { | ||||
| enum EthernetType { | ||||
|   ETHERNET_TYPE_LAN8720 = 0, | ||||
|   ETHERNET_TYPE_TLK110, | ||||
|   ETHERNET_TYPE_IP101, | ||||
| }; | ||||
|  | ||||
| struct ManualIP { | ||||
|   | ||||
							
								
								
									
										5
									
								
								esphome/components/factory_reset/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								esphome/components/factory_reset/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| import esphome.codegen as cg | ||||
|  | ||||
| CODEOWNERS = ["@anatoly-savchenkov"] | ||||
|  | ||||
| factory_reset_ns = cg.esphome_ns.namespace("factory_reset") | ||||
							
								
								
									
										30
									
								
								esphome/components/factory_reset/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/factory_reset/button/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import button | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_RESTART, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_RESTART_ALERT, | ||||
| ) | ||||
| from .. import factory_reset_ns | ||||
|  | ||||
| FactoryResetButton = factory_reset_ns.class_( | ||||
|     "FactoryResetButton", button.Button, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     button.button_schema( | ||||
|         device_class=DEVICE_CLASS_RESTART, | ||||
|         entity_category=ENTITY_CATEGORY_CONFIG, | ||||
|         icon=ICON_RESTART_ALERT, | ||||
|     ) | ||||
|     .extend({cv.GenerateID(): cv.declare_id(FactoryResetButton)}) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await button.register_button(var, config) | ||||
| @@ -0,0 +1,21 @@ | ||||
| #include "factory_reset_button.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
|  | ||||
| static const char *const TAG = "factory_reset.button"; | ||||
|  | ||||
| void FactoryResetButton::dump_config() { LOG_BUTTON("", "Factory Reset Button", this); } | ||||
| void FactoryResetButton::press_action() { | ||||
|   ESP_LOGI(TAG, "Resetting to factory defaults..."); | ||||
|   // Let MQTT settle a bit | ||||
|   delay(100);  // NOLINT | ||||
|   global_preferences->reset(); | ||||
|   App.safe_reboot(); | ||||
| } | ||||
|  | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/button/button.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
|  | ||||
| class FactoryResetButton : public button::Button, public Component { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   void press_action() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
							
								
								
									
										35
									
								
								esphome/components/factory_reset/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								esphome/components/factory_reset/switch/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import switch | ||||
| from esphome.const import ( | ||||
|     CONF_ENTITY_CATEGORY, | ||||
|     CONF_ID, | ||||
|     CONF_INVERTED, | ||||
|     CONF_ICON, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_RESTART_ALERT, | ||||
| ) | ||||
| from .. import factory_reset_ns | ||||
|  | ||||
| FactoryResetSwitch = factory_reset_ns.class_( | ||||
|     "FactoryResetSwitch", switch.Switch, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(FactoryResetSwitch), | ||||
|         cv.Optional(CONF_INVERTED): cv.invalid( | ||||
|             "Factory Reset switches do not support inverted mode!" | ||||
|         ), | ||||
|         cv.Optional(CONF_ICON, default=ICON_RESTART_ALERT): cv.icon, | ||||
|         cv.Optional( | ||||
|             CONF_ENTITY_CATEGORY, default=ENTITY_CATEGORY_CONFIG | ||||
|         ): cv.entity_category, | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await switch.register_switch(var, config) | ||||
| @@ -0,0 +1,26 @@ | ||||
| #include "factory_reset_switch.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/application.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
|  | ||||
| static const char *const TAG = "factory_reset.switch"; | ||||
|  | ||||
| void FactoryResetSwitch::dump_config() { LOG_SWITCH("", "Factory Reset Switch", this); } | ||||
| void FactoryResetSwitch::write_state(bool state) { | ||||
|   // Acknowledge | ||||
|   this->publish_state(false); | ||||
|  | ||||
|   if (state) { | ||||
|     ESP_LOGI(TAG, "Resetting to factory defaults..."); | ||||
|     // Let MQTT settle a bit | ||||
|     delay(100);  // NOLINT | ||||
|     global_preferences->reset(); | ||||
|     App.safe_reboot(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
| @@ -0,0 +1,18 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/switch/switch.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace factory_reset { | ||||
|  | ||||
| class FactoryResetSwitch : public switch_::Switch, public Component { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool state) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace factory_reset | ||||
| }  // namespace esphome | ||||
| @@ -51,7 +51,7 @@ void FingerprintGrowComponent::update() { | ||||
| void FingerprintGrowComponent::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader..."); | ||||
|   if (this->check_password_()) { | ||||
|     if (this->new_password_ != nullptr) { | ||||
|     if (this->new_password_ != -1) { | ||||
|       if (this->set_password_()) | ||||
|         return; | ||||
|     } else { | ||||
| @@ -202,9 +202,9 @@ bool FingerprintGrowComponent::check_password_() { | ||||
| } | ||||
|  | ||||
| bool FingerprintGrowComponent::set_password_() { | ||||
|   ESP_LOGI(TAG, "Setting new password: %d", *this->new_password_); | ||||
|   this->data_ = {SET_PASSWORD, (uint8_t)(*this->new_password_ >> 24), (uint8_t)(*this->new_password_ >> 16), | ||||
|                  (uint8_t)(*this->new_password_ >> 8), (uint8_t)(*this->new_password_ & 0xFF)}; | ||||
|   ESP_LOGI(TAG, "Setting new password: %d", this->new_password_); | ||||
|   this->data_ = {SET_PASSWORD, (uint8_t)(this->new_password_ >> 24), (uint8_t)(this->new_password_ >> 16), | ||||
|                  (uint8_t)(this->new_password_ >> 8), (uint8_t)(this->new_password_ & 0xFF)}; | ||||
|   if (this->send_command_() == OK) { | ||||
|     ESP_LOGI(TAG, "New password successfully set"); | ||||
|     ESP_LOGI(TAG, "Define the new password in your configuration and reflash now"); | ||||
|   | ||||
| @@ -96,7 +96,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic | ||||
|   } | ||||
|   void set_sensing_pin(GPIOPin *sensing_pin) { this->sensing_pin_ = sensing_pin; } | ||||
|   void set_password(uint32_t password) { this->password_ = password; } | ||||
|   void set_new_password(uint32_t new_password) { this->new_password_ = &new_password; } | ||||
|   void set_new_password(uint32_t new_password) { this->new_password_ = new_password; } | ||||
|   void set_fingerprint_count_sensor(sensor::Sensor *fingerprint_count_sensor) { | ||||
|     this->fingerprint_count_sensor_ = fingerprint_count_sensor; | ||||
|   } | ||||
| @@ -153,7 +153,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic | ||||
|   uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF}; | ||||
|   uint16_t capacity_ = 64; | ||||
|   uint32_t password_ = 0x0; | ||||
|   uint32_t *new_password_{nullptr}; | ||||
|   uint32_t new_password_ = -1; | ||||
|   GPIOPin *sensing_pin_{nullptr}; | ||||
|   uint8_t enrollment_image_ = 0; | ||||
|   uint16_t enrollment_slot_ = 0; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import functools | ||||
| from pathlib import Path | ||||
| import hashlib | ||||
| import os | ||||
| import re | ||||
|  | ||||
| import requests | ||||
| @@ -9,6 +10,7 @@ from esphome import core | ||||
| from esphome.components import display | ||||
| import esphome.config_validation as cv | ||||
| import esphome.codegen as cg | ||||
| from esphome.helpers import copy_file_if_changed | ||||
| from esphome.const import ( | ||||
|     CONF_FAMILY, | ||||
|     CONF_FILE, | ||||
| @@ -88,21 +90,33 @@ def validate_truetype_file(value): | ||||
|     return cv.file_(value) | ||||
|  | ||||
|  | ||||
| def _compute_gfonts_local_path(value) -> Path: | ||||
|     name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" | ||||
| def _compute_local_font_dir(name) -> Path: | ||||
|     base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN | ||||
|     h = hashlib.new("sha256") | ||||
|     h.update(name.encode()) | ||||
|     return base_dir / h.hexdigest()[:8] / "font.ttf" | ||||
|     return base_dir / h.hexdigest()[:8] | ||||
|  | ||||
|  | ||||
| def _compute_gfonts_local_path(value) -> Path: | ||||
|     name = f"{value[CONF_FAMILY]}@{value[CONF_WEIGHT]}@{value[CONF_ITALIC]}@v1" | ||||
|     return _compute_local_font_dir(name) / "font.ttf" | ||||
|  | ||||
|  | ||||
| TYPE_LOCAL = "local" | ||||
| TYPE_LOCAL_BITMAP = "local_bitmap" | ||||
| TYPE_GFONTS = "gfonts" | ||||
| LOCAL_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_PATH): validate_truetype_file, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| LOCAL_BITMAP_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_PATH): cv.file_, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| CONF_ITALIC = "italic" | ||||
| FONT_WEIGHTS = { | ||||
|     "thin": 100, | ||||
| @@ -132,7 +146,7 @@ def download_gfonts(value): | ||||
|     if path.is_file(): | ||||
|         return value | ||||
|     try: | ||||
|         req = requests.get(url) | ||||
|         req = requests.get(url, timeout=30) | ||||
|         req.raise_for_status() | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         raise cv.Invalid( | ||||
| @@ -148,7 +162,7 @@ def download_gfonts(value): | ||||
|  | ||||
|     ttf_url = match.group(1) | ||||
|     try: | ||||
|         req = requests.get(ttf_url) | ||||
|         req = requests.get(ttf_url, timeout=30) | ||||
|         req.raise_for_status() | ||||
|     except requests.exceptions.RequestException as e: | ||||
|         raise cv.Invalid(f"Could not download ttf file for {name} ({ttf_url}): {e}") | ||||
| @@ -185,6 +199,15 @@ def validate_file_shorthand(value): | ||||
|         if weight is not None: | ||||
|             data[CONF_WEIGHT] = weight[1:] | ||||
|         return FILE_SCHEMA(data) | ||||
|  | ||||
|     if value.endswith(".pcf") or value.endswith(".bdf"): | ||||
|         return FILE_SCHEMA( | ||||
|             { | ||||
|                 CONF_TYPE: TYPE_LOCAL_BITMAP, | ||||
|                 CONF_PATH: value, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return FILE_SCHEMA( | ||||
|         { | ||||
|             CONF_TYPE: TYPE_LOCAL, | ||||
| @@ -197,6 +220,7 @@ TYPED_FILE_SCHEMA = cv.typed_schema( | ||||
|     { | ||||
|         TYPE_LOCAL: LOCAL_SCHEMA, | ||||
|         TYPE_GFONTS: GFONTS_SCHEMA, | ||||
|         TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA, | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -228,27 +252,121 @@ FONT_SCHEMA = cv.Schema( | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All(validate_pillow_installed, FONT_SCHEMA) | ||||
|  | ||||
| # PIL doesn't provide a consistent interface for both TrueType and bitmap | ||||
| # fonts. So, we use our own wrappers to give us the consistency that we need. | ||||
|  | ||||
| async def to_code(config): | ||||
|  | ||||
| class TrueTypeFontWrapper: | ||||
|     def __init__(self, font): | ||||
|         self.font = font | ||||
|  | ||||
|     def getoffset(self, glyph): | ||||
|         _, (offset_x, offset_y) = self.font.font.getsize(glyph) | ||||
|         return offset_x, offset_y | ||||
|  | ||||
|     def getmask(self, glyph, **kwargs): | ||||
|         return self.font.getmask(glyph, **kwargs) | ||||
|  | ||||
|     def getmetrics(self, glyphs): | ||||
|         return self.font.getmetrics() | ||||
|  | ||||
|  | ||||
| class BitmapFontWrapper: | ||||
|     def __init__(self, font): | ||||
|         self.font = font | ||||
|         self.max_height = 0 | ||||
|  | ||||
|     def getoffset(self, glyph): | ||||
|         return 0, 0 | ||||
|  | ||||
|     def getmask(self, glyph, **kwargs): | ||||
|         return self.font.getmask(glyph, **kwargs) | ||||
|  | ||||
|     def getmetrics(self, glyphs): | ||||
|         max_height = 0 | ||||
|         for glyph in glyphs: | ||||
|             mask = self.getmask(glyph, mode="1") | ||||
|             _, height = mask.size | ||||
|             if height > max_height: | ||||
|                 max_height = height | ||||
|         return (max_height, 0) | ||||
|  | ||||
|  | ||||
| def convert_bitmap_to_pillow_font(filepath): | ||||
|     from PIL import PcfFontFile, BdfFontFile | ||||
|  | ||||
|     local_bitmap_font_file = _compute_local_font_dir(filepath) / os.path.basename( | ||||
|         filepath | ||||
|     ) | ||||
|  | ||||
|     copy_file_if_changed(filepath, local_bitmap_font_file) | ||||
|  | ||||
|     with open(local_bitmap_font_file, "rb") as fp: | ||||
|         try: | ||||
|             try: | ||||
|                 p = PcfFontFile.PcfFontFile(fp) | ||||
|             except SyntaxError: | ||||
|                 fp.seek(0) | ||||
|                 p = BdfFontFile.BdfFontFile(fp) | ||||
|  | ||||
|             # Convert to pillow-formatted fonts, which have a .pil and .pbm extension. | ||||
|             p.save(local_bitmap_font_file) | ||||
|         except (SyntaxError, OSError) as err: | ||||
|             raise core.EsphomeError( | ||||
|                 f"Failed to parse as bitmap font: '{filepath}': {err}" | ||||
|             ) | ||||
|  | ||||
|     local_pil_font_file = os.path.splitext(local_bitmap_font_file)[0] + ".pil" | ||||
|     return cv.file_(local_pil_font_file) | ||||
|  | ||||
|  | ||||
| def load_bitmap_font(filepath): | ||||
|     from PIL import ImageFont | ||||
|  | ||||
|     conf = config[CONF_FILE] | ||||
|     if conf[CONF_TYPE] == TYPE_LOCAL: | ||||
|         path = CORE.relative_config_path(conf[CONF_PATH]) | ||||
|     elif conf[CONF_TYPE] == TYPE_GFONTS: | ||||
|         path = _compute_gfonts_local_path(conf) | ||||
|     # Convert bpf and pcf files to pillow fonts, first. | ||||
|     pil_font_path = convert_bitmap_to_pillow_font(filepath) | ||||
|  | ||||
|     try: | ||||
|         font = ImageFont.truetype(str(path), config[CONF_SIZE]) | ||||
|         font = ImageFont.load(str(pil_font_path)) | ||||
|     except Exception as e: | ||||
|         raise core.EsphomeError( | ||||
|             f"Failed to load bitmap font file: {pil_font_path} : {e}" | ||||
|         ) | ||||
|  | ||||
|     return BitmapFontWrapper(font) | ||||
|  | ||||
|  | ||||
| def load_ttf_font(path, size): | ||||
|     from PIL import ImageFont | ||||
|  | ||||
|     try: | ||||
|         font = ImageFont.truetype(str(path), size) | ||||
|     except Exception as e: | ||||
|         raise core.EsphomeError(f"Could not load truetype file {path}: {e}") | ||||
|  | ||||
|     ascent, descent = font.getmetrics() | ||||
|     return TrueTypeFontWrapper(font) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     conf = config[CONF_FILE] | ||||
|     if conf[CONF_TYPE] == TYPE_LOCAL_BITMAP: | ||||
|         font = load_bitmap_font(CORE.relative_config_path(conf[CONF_PATH])) | ||||
|     elif conf[CONF_TYPE] == TYPE_LOCAL: | ||||
|         path = CORE.relative_config_path(conf[CONF_PATH]) | ||||
|         font = load_ttf_font(path, config[CONF_SIZE]) | ||||
|     elif conf[CONF_TYPE] == TYPE_GFONTS: | ||||
|         path = _compute_gfonts_local_path(conf) | ||||
|         font = load_ttf_font(path, config[CONF_SIZE]) | ||||
|     else: | ||||
|         raise core.EsphomeError(f"Could not load font: unknown type: {conf[CONF_TYPE]}") | ||||
|  | ||||
|     ascent, descent = font.getmetrics(config[CONF_GLYPHS]) | ||||
|  | ||||
|     glyph_args = {} | ||||
|     data = [] | ||||
|     for glyph in config[CONF_GLYPHS]: | ||||
|         mask = font.getmask(glyph, mode="1") | ||||
|         _, (offset_x, offset_y) = font.font.getsize(glyph) | ||||
|         offset_x, offset_y = font.getoffset(glyph) | ||||
|         width, height = mask.size | ||||
|         width8 = ((width + 7) // 8) * 8 | ||||
|         glyph_data = [0] * (height * width8 // 8) | ||||
|   | ||||
| @@ -2,7 +2,7 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import pins | ||||
| from esphome.components import switch | ||||
| from esphome.const import CONF_ID, CONF_INTERLOCK, CONF_PIN, CONF_RESTORE_MODE | ||||
| from esphome.const import CONF_INTERLOCK, CONF_PIN, CONF_RESTORE_MODE | ||||
| from .. import gpio_ns | ||||
|  | ||||
| GPIOSwitch = gpio_ns.class_("GPIOSwitch", switch.Switch, cg.Component) | ||||
| @@ -18,25 +18,27 @@ RESTORE_MODES = { | ||||
| } | ||||
|  | ||||
| CONF_INTERLOCK_WAIT_TIME = "interlock_wait_time" | ||||
| CONFIG_SCHEMA = switch.SWITCH_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(GPIOSwitch), | ||||
|         cv.Required(CONF_PIN): pins.gpio_output_pin_schema, | ||||
|         cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( | ||||
|             RESTORE_MODES, upper=True, space="_" | ||||
|         ), | ||||
|         cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), | ||||
|         cv.Optional( | ||||
|             CONF_INTERLOCK_WAIT_TIME, default="0ms" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
| CONFIG_SCHEMA = ( | ||||
|     switch.switch_schema(GPIOSwitch) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.Required(CONF_PIN): pins.gpio_output_pin_schema, | ||||
|             cv.Optional(CONF_RESTORE_MODE, default="RESTORE_DEFAULT_OFF"): cv.enum( | ||||
|                 RESTORE_MODES, upper=True, space="_" | ||||
|             ), | ||||
|             cv.Optional(CONF_INTERLOCK): cv.ensure_list(cv.use_id(switch.Switch)), | ||||
|             cv.Optional( | ||||
|                 CONF_INTERLOCK_WAIT_TIME, default="0ms" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     var = await switch.new_switch(config) | ||||
|     await cg.register_component(var, config) | ||||
|     await switch.register_switch(var, config) | ||||
|  | ||||
|     pin = await cg.gpio_pin_expression(config[CONF_PIN]) | ||||
|     cg.add(var.set_pin(pin)) | ||||
|   | ||||
| @@ -16,8 +16,17 @@ enum HLW8012SensorModels { | ||||
|   HLW8012_SENSOR_MODEL_BL0937 | ||||
| }; | ||||
|  | ||||
| #ifdef HAS_PCNT | ||||
| #define USE_PCNT true | ||||
| #else | ||||
| #define USE_PCNT false | ||||
| #endif | ||||
|  | ||||
| class HLW8012Component : public PollingComponent { | ||||
|  public: | ||||
|   HLW8012Component() | ||||
|       : cf_store_(*pulse_counter::get_storage(USE_PCNT)), cf1_store_(*pulse_counter::get_storage(USE_PCNT)) {} | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
| @@ -49,9 +58,9 @@ class HLW8012Component : public PollingComponent { | ||||
|   uint64_t cf_total_pulses_{0}; | ||||
|   GPIOPin *sel_pin_; | ||||
|   InternalGPIOPin *cf_pin_; | ||||
|   pulse_counter::PulseCounterStorage cf_store_; | ||||
|   pulse_counter::PulseCounterStorageBase &cf_store_; | ||||
|   InternalGPIOPin *cf1_pin_; | ||||
|   pulse_counter::PulseCounterStorage cf1_store_; | ||||
|   pulse_counter::PulseCounterStorageBase &cf1_store_; | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_{nullptr}; | ||||
|   sensor::Sensor *power_sensor_{nullptr}; | ||||
|   | ||||
| @@ -4,12 +4,15 @@ from esphome.components import binary_sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_COLD, | ||||
|     DEVICE_CLASS_PROBLEM, | ||||
| ) | ||||
|  | ||||
| from . import hydreon_rgxx_ns, HydreonRGxxComponent | ||||
|  | ||||
| CONF_HYDREON_RGXX_ID = "hydreon_rgxx_id" | ||||
| CONF_TOO_COLD = "too_cold" | ||||
| CONF_LENS_BAD = "lens_bad" | ||||
| CONF_EM_SAT = "em_sat" | ||||
|  | ||||
| HydreonRGxxBinarySensor = hydreon_rgxx_ns.class_( | ||||
|     "HydreonRGxxBinaryComponent", cg.Component | ||||
| @@ -23,6 +26,12 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|         cv.Optional(CONF_TOO_COLD): binary_sensor.binary_sensor_schema( | ||||
|             device_class=DEVICE_CLASS_COLD | ||||
|         ), | ||||
|         cv.Optional(CONF_LENS_BAD): binary_sensor.binary_sensor_schema( | ||||
|             device_class=DEVICE_CLASS_PROBLEM, | ||||
|         ), | ||||
|         cv.Optional(CONF_EM_SAT): binary_sensor.binary_sensor_schema( | ||||
|             device_class=DEVICE_CLASS_PROBLEM, | ||||
|         ), | ||||
|     } | ||||
| ) | ||||
|  | ||||
| @@ -31,6 +40,14 @@ async def to_code(config): | ||||
|     main_sensor = await cg.get_variable(config[CONF_HYDREON_RGXX_ID]) | ||||
|     bin_component = cg.new_Pvariable(config[CONF_ID], main_sensor) | ||||
|     await cg.register_component(bin_component, config) | ||||
|     if CONF_TOO_COLD in config: | ||||
|         tc = await binary_sensor.new_binary_sensor(config[CONF_TOO_COLD]) | ||||
|         cg.add(main_sensor.set_too_cold_sensor(tc)) | ||||
|  | ||||
|     mapping = { | ||||
|         CONF_TOO_COLD: main_sensor.set_too_cold_sensor, | ||||
|         CONF_LENS_BAD: main_sensor.set_lens_bad_sensor, | ||||
|         CONF_EM_SAT: main_sensor.set_em_sat_sensor, | ||||
|     } | ||||
|  | ||||
|     for key, value in mapping.items(): | ||||
|         if key in config: | ||||
|             sensor = await binary_sensor.new_binary_sensor(config[key]) | ||||
|             cg.add(value(sensor)) | ||||
|   | ||||
| @@ -9,6 +9,7 @@ static const int MAX_DATA_LENGTH_BYTES = 80; | ||||
| static const uint8_t ASCII_LF = 0x0A; | ||||
| #define HYDREON_RGXX_COMMA , | ||||
| static const char *const PROTOCOL_NAMES[] = {HYDREON_RGXX_PROTOCOL_LIST(, HYDREON_RGXX_COMMA)}; | ||||
| static const char *const IGNORE_STRINGS[] = {HYDREON_RGXX_IGNORE_LIST(, HYDREON_RGXX_COMMA)}; | ||||
|  | ||||
| void HydreonRGxxComponent::dump_config() { | ||||
|   this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8); | ||||
| @@ -34,33 +35,37 @@ void HydreonRGxxComponent::setup() { | ||||
|   this->schedule_reboot_(); | ||||
| } | ||||
|  | ||||
| bool HydreonRGxxComponent::sensor_missing_() { | ||||
| int HydreonRGxxComponent::num_sensors_missing_() { | ||||
|   if (this->sensors_received_ == -1) { | ||||
|     // no request sent yet, don't check | ||||
|     return false; | ||||
|   } else { | ||||
|     if (this->sensors_received_ == 0) { | ||||
|       ESP_LOGW(TAG, "No data at all"); | ||||
|       return true; | ||||
|     } | ||||
|     for (int i = 0; i < NUM_SENSORS; i++) { | ||||
|       if (this->sensors_[i] == nullptr) { | ||||
|         continue; | ||||
|       } | ||||
|       if ((this->sensors_received_ >> i & 1) == 0) { | ||||
|         ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]); | ||||
|         return true; | ||||
|       } | ||||
|     } | ||||
|     return false; | ||||
|     return -1; | ||||
|   } | ||||
|   int ret = NUM_SENSORS; | ||||
|   for (int i = 0; i < NUM_SENSORS; i++) { | ||||
|     if (this->sensors_[i] == nullptr) { | ||||
|       ret -= 1; | ||||
|       continue; | ||||
|     } | ||||
|     if ((this->sensors_received_ >> i & 1) != 0) { | ||||
|       ret -= 1; | ||||
|     } | ||||
|   } | ||||
|   return ret; | ||||
| } | ||||
|  | ||||
| void HydreonRGxxComponent::update() { | ||||
|   if (this->boot_count_ > 0) { | ||||
|     if (this->sensor_missing_()) { | ||||
|     if (this->num_sensors_missing_() > 0) { | ||||
|       for (int i = 0; i < NUM_SENSORS; i++) { | ||||
|         if (this->sensors_[i] == nullptr) { | ||||
|           continue; | ||||
|         } | ||||
|         if ((this->sensors_received_ >> i & 1) == 0) { | ||||
|           ESP_LOGW(TAG, "Missing %s", PROTOCOL_NAMES[i]); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       this->no_response_count_++; | ||||
|       ESP_LOGE(TAG, "data missing %d times", this->no_response_count_); | ||||
|       ESP_LOGE(TAG, "missing %d sensors; %d times in a row", this->num_sensors_missing_(), this->no_response_count_); | ||||
|       if (this->no_response_count_ > 15) { | ||||
|         ESP_LOGE(TAG, "asking sensor to reboot"); | ||||
|         for (auto &sensor : this->sensors_) { | ||||
| @@ -79,8 +84,16 @@ void HydreonRGxxComponent::update() { | ||||
|     if (this->too_cold_sensor_ != nullptr) { | ||||
|       this->too_cold_sensor_->publish_state(this->too_cold_); | ||||
|     } | ||||
|     if (this->lens_bad_sensor_ != nullptr) { | ||||
|       this->lens_bad_sensor_->publish_state(this->lens_bad_); | ||||
|     } | ||||
|     if (this->em_sat_sensor_ != nullptr) { | ||||
|       this->em_sat_sensor_->publish_state(this->em_sat_); | ||||
|     } | ||||
| #endif | ||||
|     this->too_cold_ = false; | ||||
|     this->lens_bad_ = false; | ||||
|     this->em_sat_ = false; | ||||
|     this->sensors_received_ = 0; | ||||
|   } | ||||
| } | ||||
| @@ -146,6 +159,25 @@ void HydreonRGxxComponent::process_line_() { | ||||
|     ESP_LOGI(TAG, "Comment: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); | ||||
|     return; | ||||
|   } | ||||
|   std::string::size_type newlineposn = this->buffer_.find('\n'); | ||||
|   if (newlineposn <= 1) { | ||||
|     // allow both \r\n and \n | ||||
|     ESP_LOGD(TAG, "Received empty line"); | ||||
|     return; | ||||
|   } | ||||
|   if (newlineposn <= 2) { | ||||
|     // single character lines, such as acknowledgements | ||||
|     ESP_LOGD(TAG, "Received ack: %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (this->buffer_.find("LensBad") != std::string::npos) { | ||||
|     ESP_LOGW(TAG, "Received LensBad!"); | ||||
|     this->lens_bad_ = true; | ||||
|   } | ||||
|   if (this->buffer_.find("EmSat") != std::string::npos) { | ||||
|     ESP_LOGW(TAG, "Received EmSat!"); | ||||
|     this->em_sat_ = true; | ||||
|   } | ||||
|   if (this->buffer_starts_with_("PwrDays")) { | ||||
|     if (this->boot_count_ <= 0) { | ||||
|       this->boot_count_ = 1; | ||||
| @@ -200,7 +232,16 @@ void HydreonRGxxComponent::process_line_() { | ||||
|       ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); | ||||
|       this->sensors_received_ |= (1 << i); | ||||
|     } | ||||
|     if (this->request_temperature_ && this->num_sensors_missing_() == 1) { | ||||
|       this->write_str("T\n"); | ||||
|     } | ||||
|   } else { | ||||
|     for (const auto *ignore : IGNORE_STRINGS) { | ||||
|       if (this->buffer_starts_with_(ignore)) { | ||||
|         ESP_LOGI(TAG, "Ignoring %s", this->buffer_.substr(0, this->buffer_.size() - 2).c_str()); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     ESP_LOGI(TAG, "Got unknown line: %s", this->buffer_.c_str()); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -26,13 +26,18 @@ static const uint8_t NUM_SENSORS = 1; | ||||
| #define HYDREON_RGXX_PROTOCOL_LIST(F, SEP) F("") | ||||
| #endif | ||||
|  | ||||
| #define HYDREON_RGXX_IGNORE_LIST(F, SEP) F("Emitters") SEP F("Event") SEP F("Reset") | ||||
|  | ||||
| class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { | ||||
|  public: | ||||
|   void set_sensor(sensor::Sensor *sensor, int index) { this->sensors_[index] = sensor; } | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   void set_too_cold_sensor(binary_sensor::BinarySensor *sensor) { this->too_cold_sensor_ = sensor; } | ||||
|   void set_lens_bad_sensor(binary_sensor::BinarySensor *sensor) { this->lens_bad_sensor_ = sensor; } | ||||
|   void set_em_sat_sensor(binary_sensor::BinarySensor *sensor) { this->em_sat_sensor_ = sensor; } | ||||
| #endif | ||||
|   void set_model(RGModel model) { model_ = model; } | ||||
|   void set_request_temperature(bool b) { request_temperature_ = b; } | ||||
|  | ||||
|   /// Schedule data readings. | ||||
|   void update() override; | ||||
| @@ -49,11 +54,13 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { | ||||
|   void schedule_reboot_(); | ||||
|   bool buffer_starts_with_(const std::string &prefix); | ||||
|   bool buffer_starts_with_(const char *prefix); | ||||
|   bool sensor_missing_(); | ||||
|   int num_sensors_missing_(); | ||||
|  | ||||
|   sensor::Sensor *sensors_[NUM_SENSORS] = {nullptr}; | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   binary_sensor::BinarySensor *too_cold_sensor_ = nullptr; | ||||
|   binary_sensor::BinarySensor *lens_bad_sensor_ = nullptr; | ||||
|   binary_sensor::BinarySensor *em_sat_sensor_ = nullptr; | ||||
| #endif | ||||
|  | ||||
|   int16_t boot_count_ = 0; | ||||
| @@ -62,6 +69,9 @@ class HydreonRGxxComponent : public PollingComponent, public uart::UARTDevice { | ||||
|   RGModel model_ = RG9; | ||||
|   int sw_version_ = 0; | ||||
|   bool too_cold_ = false; | ||||
|   bool lens_bad_ = false; | ||||
|   bool em_sat_ = false; | ||||
|   bool request_temperature_ = false; | ||||
|  | ||||
|   // bit field showing which sensors we have received data for | ||||
|   int sensors_received_ = -1; | ||||
|   | ||||
| @@ -5,8 +5,11 @@ from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_MODEL, | ||||
|     CONF_MOISTURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     ICON_THERMOMETER, | ||||
| ) | ||||
|  | ||||
| from . import RGModel, HydreonRGxxComponent | ||||
| @@ -33,6 +36,7 @@ SUPPORTED_SENSORS = { | ||||
|     CONF_TOTAL_ACC: ["RG_15"], | ||||
|     CONF_R_INT: ["RG_15"], | ||||
|     CONF_MOISTURE: ["RG_9"], | ||||
|     CONF_TEMPERATURE: ["RG_9"], | ||||
| } | ||||
| PROTOCOL_NAMES = { | ||||
|     CONF_MOISTURE: "R", | ||||
| @@ -40,6 +44,7 @@ PROTOCOL_NAMES = { | ||||
|     CONF_R_INT: "RInt", | ||||
|     CONF_EVENT_ACC: "EventAcc", | ||||
|     CONF_TOTAL_ACC: "TotalAcc", | ||||
|     CONF_TEMPERATURE: "t", | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -92,6 +97,12 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=0, | ||||
|                 icon=ICON_THERMOMETER, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
| @@ -108,7 +119,7 @@ async def to_code(config): | ||||
|     cg.add_define( | ||||
|         "HYDREON_RGXX_PROTOCOL_LIST(F, sep)", | ||||
|         cg.RawExpression( | ||||
|             " sep ".join([f'F("{name}")' for name in PROTOCOL_NAMES.values()]) | ||||
|             " sep ".join([f'F("{name} ")' for name in PROTOCOL_NAMES.values()]) | ||||
|         ), | ||||
|     ) | ||||
|     cg.add_define("HYDREON_RGXX_NUM_SENSORS", len(PROTOCOL_NAMES)) | ||||
| @@ -117,3 +128,5 @@ async def to_code(config): | ||||
|         if conf in config: | ||||
|             sens = await sensor.new_sensor(config[conf]) | ||||
|             cg.add(var.set_sensor(sens, i)) | ||||
|  | ||||
|     cg.add(var.set_request_temperature(CONF_TEMPERATURE in config)) | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| import esphome.final_validate as fv | ||||
| from esphome import pins | ||||
| from esphome.const import ( | ||||
|     CONF_FREQUENCY, | ||||
| @@ -110,3 +111,27 @@ async def register_i2c_device(var, config): | ||||
|     parent = await cg.get_variable(config[CONF_I2C_ID]) | ||||
|     cg.add(var.set_i2c_bus(parent)) | ||||
|     cg.add(var.set_i2c_address(config[CONF_ADDRESS])) | ||||
|  | ||||
|  | ||||
| def final_validate_device_schema( | ||||
|     name: str, *, min_frequency: cv.frequency = None, max_frequency: cv.frequency = None | ||||
| ): | ||||
|     hub_schema = {} | ||||
|     if min_frequency is not None: | ||||
|         hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( | ||||
|             min=cv.frequency(min_frequency), | ||||
|             min_included=True, | ||||
|             msg=f"Component {name} requires a minimum frequency of {min_frequency} for the I2C bus", | ||||
|         ) | ||||
|  | ||||
|     if max_frequency is not None: | ||||
|         hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range( | ||||
|             max=cv.frequency(max_frequency), | ||||
|             max_included=True, | ||||
|             msg=f"Component {name} cannot be used with a frequency of over {max_frequency} for the I2C bus", | ||||
|         ) | ||||
|  | ||||
|     return cv.Schema( | ||||
|         {cv.Required(CONF_I2C_ID): fv.id_declaration_match_schema(hub_schema)}, | ||||
|         extra=cv.ALLOW_EXTRA, | ||||
|     ) | ||||
|   | ||||
| @@ -10,7 +10,6 @@ namespace ili9341 { | ||||
| static const char *const TAG = "ili9341"; | ||||
|  | ||||
| void ILI9341Display::setup_pins_() { | ||||
|   this->init_internal_(this->get_buffer_length_()); | ||||
|   this->dc_pin_->setup();  // OUTPUT | ||||
|   this->dc_pin_->digital_write(false); | ||||
|   if (this->reset_pin_ != nullptr) { | ||||
| @@ -28,15 +27,14 @@ void ILI9341Display::setup_pins_() { | ||||
|  | ||||
| void ILI9341Display::dump_config() { | ||||
|   LOG_DISPLAY("", "ili9341", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Width: %d, Height: %d,  Rotation: %d", this->width_, this->height_, this->rotation_); | ||||
|   LOG_PIN("  Reset Pin: ", this->reset_pin_); | ||||
|   LOG_PIN("  DC Pin: ", this->dc_pin_); | ||||
|   LOG_PIN("  Busy Pin: ", this->busy_pin_); | ||||
|   LOG_PIN("  Backlight Pin: ", this->led_pin_); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| float ILI9341Display::get_setup_priority() const { return setup_priority::PROCESSOR; } | ||||
| float ILI9341Display::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
|  | ||||
| void ILI9341Display::command(uint8_t value) { | ||||
|   this->start_command_(); | ||||
|   this->write_byte(value); | ||||
| @@ -88,10 +86,19 @@ void ILI9341Display::display_() { | ||||
|   // we will only update the changed window to the display | ||||
|   uint16_t w = this->x_high_ - this->x_low_ + 1; | ||||
|   uint16_t h = this->y_high_ - this->y_low_ + 1; | ||||
|   uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); | ||||
|  | ||||
|   // check if something was displayed | ||||
|   if ((this->x_high_ < this->x_low_) || (this->y_high_ < this->y_low_)) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   set_addr_window_(this->x_low_, this->y_low_, w, h); | ||||
|  | ||||
|   ESP_LOGVV("ILI9341", "Start ILI9341Display::display_(xl:%d, xh:%d, yl:%d, yh:%d, w:%d, h:%d, start_pos:%d)", | ||||
|             this->x_low_, this->x_high_, this->y_low_, this->y_high_, w, h, start_pos); | ||||
|  | ||||
|   this->start_data_(); | ||||
|   uint32_t start_pos = ((this->y_low_ * this->width_) + x_low_); | ||||
|   for (uint16_t row = 0; row < h; row++) { | ||||
|     uint32_t pos = start_pos + (row * width_); | ||||
|     uint32_t rem = w; | ||||
| @@ -101,7 +108,9 @@ void ILI9341Display::display_() { | ||||
|       this->write_array(transfer_buffer_, 2 * sz); | ||||
|       pos += sz; | ||||
|       rem -= sz; | ||||
|       App.feed_wdt(); | ||||
|     } | ||||
|     App.feed_wdt(); | ||||
|   } | ||||
|   this->end_data_(); | ||||
|  | ||||
| @@ -121,20 +130,10 @@ void ILI9341Display::fill(Color color) { | ||||
|   this->y_high_ = this->get_height_internal() - 1; | ||||
| } | ||||
|  | ||||
| void ILI9341Display::fill_internal_(Color color) { | ||||
|   if (color.raw_32 == Color::BLACK.raw_32) { | ||||
|     memset(transfer_buffer_, 0, sizeof(transfer_buffer_)); | ||||
|   } else { | ||||
|     uint8_t *dst = transfer_buffer_; | ||||
|     auto color565 = display::ColorUtil::color_to_565(color); | ||||
| void ILI9341Display::fill_internal_(uint8_t color) { | ||||
|   memset(transfer_buffer_, color, sizeof(transfer_buffer_)); | ||||
|  | ||||
|     while (dst < transfer_buffer_ + sizeof(transfer_buffer_)) { | ||||
|       *dst++ = (uint8_t)(color565 >> 8); | ||||
|       *dst++ = (uint8_t) color565; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   uint32_t rem = this->get_width_internal() * this->get_height_internal(); | ||||
|   uint32_t rem = (this->get_buffer_length_() * 2); | ||||
|  | ||||
|   this->set_addr_window_(0, 0, this->get_width_internal(), this->get_height_internal()); | ||||
|   this->start_data_(); | ||||
| @@ -147,26 +146,58 @@ void ILI9341Display::fill_internal_(Color color) { | ||||
|  | ||||
|   this->end_data_(); | ||||
|  | ||||
|   memset(buffer_, 0, (this->get_width_internal()) * (this->get_height_internal())); | ||||
|   memset(buffer_, color, this->get_buffer_length_()); | ||||
| } | ||||
|  | ||||
| void ILI9341Display::rotate_my_(uint8_t m) { | ||||
|   uint8_t rotation = m & 3;  // can't be higher than 3 | ||||
|   switch (rotation) { | ||||
|     case 0: | ||||
|       m = (MADCTL_MX | MADCTL_BGR); | ||||
|       // _width = ILI9341_TFTWIDTH; | ||||
|       // _height = ILI9341_TFTHEIGHT; | ||||
|       break; | ||||
|     case 1: | ||||
|       m = (MADCTL_MV | MADCTL_BGR); | ||||
|       // _width = ILI9341_TFTHEIGHT; | ||||
|       // _height = ILI9341_TFTWIDTH; | ||||
|       break; | ||||
|     case 2: | ||||
|       m = (MADCTL_MY | MADCTL_BGR); | ||||
|       // _width = ILI9341_TFTWIDTH; | ||||
|       // _height = ILI9341_TFTHEIGHT; | ||||
|       break; | ||||
|     case 3: | ||||
|       m = (MADCTL_MX | MADCTL_MY | MADCTL_MV | MADCTL_BGR); | ||||
|       // _width = ILI9341_TFTHEIGHT; | ||||
|       // _height = ILI9341_TFTWIDTH; | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   this->command(ILI9341_MADCTL); | ||||
|   this->data(m); | ||||
| } | ||||
|  | ||||
| void HOT ILI9341Display::draw_absolute_pixel_internal(int x, int y, Color color) { | ||||
|   if (x >= this->get_width_internal() || x < 0 || y >= this->get_height_internal() || y < 0) | ||||
|     return; | ||||
|  | ||||
|   // low and high watermark may speed up drawing from buffer | ||||
|   this->x_low_ = (x < this->x_low_) ? x : this->x_low_; | ||||
|   this->y_low_ = (y < this->y_low_) ? y : this->y_low_; | ||||
|   this->x_high_ = (x > this->x_high_) ? x : this->x_high_; | ||||
|   this->y_high_ = (y > this->y_high_) ? y : this->y_high_; | ||||
|  | ||||
|   uint32_t pos = (y * width_) + x; | ||||
|   uint8_t new_color; | ||||
|  | ||||
|   if (this->buffer_color_mode_ == BITS_8) { | ||||
|     uint8_t color332 = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); | ||||
|     buffer_[pos] = color332; | ||||
|     new_color = display::ColorUtil::color_to_332(color, display::ColorOrder::COLOR_ORDER_RGB); | ||||
|   } else {  // if (this->buffer_color_mode_ == BITS_8_INDEXED) { | ||||
|     uint8_t index = display::ColorUtil::color_to_index8_palette888(color, this->palette_); | ||||
|     buffer_[pos] = index; | ||||
|     new_color = display::ColorUtil::color_to_index8_palette888(color, this->palette_); | ||||
|   } | ||||
|  | ||||
|   if (buffer_[pos] != new_color) { | ||||
|     buffer_[pos] = new_color; | ||||
|     // low and high watermark may speed up drawing from buffer | ||||
|     this->x_low_ = (x < this->x_low_) ? x : this->x_low_; | ||||
|     this->y_low_ = (y < this->y_low_) ? y : this->y_low_; | ||||
|     this->x_high_ = (x > this->x_high_) ? x : this->x_high_; | ||||
|     this->y_high_ = (y > this->y_high_) ? y : this->y_high_; | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -252,7 +283,6 @@ void ILI9341M5Stack::initialize() { | ||||
|   this->width_ = 320; | ||||
|   this->height_ = 240; | ||||
|   this->invert_display_(true); | ||||
|   this->fill_internal_(Color::BLACK); | ||||
| } | ||||
|  | ||||
| //   24_TFT display | ||||
| @@ -260,7 +290,6 @@ void ILI9341TFT24::initialize() { | ||||
|   this->init_lcd_(INITCMD_TFT); | ||||
|   this->width_ = 240; | ||||
|   this->height_ = 320; | ||||
|   this->fill_internal_(Color::BLACK); | ||||
| } | ||||
|  | ||||
| //   24_TFT rotated display | ||||
| @@ -268,7 +297,6 @@ void ILI9341TFT24R::initialize() { | ||||
|   this->init_lcd_(INITCMD_TFT); | ||||
|   this->width_ = 320; | ||||
|   this->height_ = 240; | ||||
|   this->fill_internal_(Color::BLACK); | ||||
| } | ||||
|  | ||||
| }  // namespace ili9341 | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
| #include "esphome/components/display/display_buffer.h" | ||||
| #include "ili9341_defines.h" | ||||
| #include "ili9341_init.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ili9341 { | ||||
| @@ -47,6 +48,14 @@ class ILI9341Display : public PollingComponent, | ||||
|   void setup() override { | ||||
|     this->setup_pins_(); | ||||
|     this->initialize(); | ||||
|  | ||||
|     this->x_low_ = this->width_; | ||||
|     this->y_low_ = this->height_; | ||||
|     this->x_high_ = 0; | ||||
|     this->y_high_ = 0; | ||||
|  | ||||
|     this->init_internal_(this->get_buffer_length_()); | ||||
|     this->fill_internal_(0x00); | ||||
|   } | ||||
|  | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
| @@ -59,8 +68,9 @@ class ILI9341Display : public PollingComponent, | ||||
|   void set_addr_window_(uint16_t x, uint16_t y, uint16_t w, uint16_t h); | ||||
|   void invert_display_(bool invert); | ||||
|   void reset_(); | ||||
|   void fill_internal_(Color color); | ||||
|   void fill_internal_(uint8_t color); | ||||
|   void display_(); | ||||
|   void rotate_my_(uint8_t m); | ||||
|  | ||||
|   ILI9341Model model_; | ||||
|   int16_t width_{320};   ///< Display width as modified by current rotation | ||||
|   | ||||
| @@ -23,6 +23,8 @@ void IntegrationSensor::setup() { | ||||
| } | ||||
| void IntegrationSensor::dump_config() { LOG_SENSOR("", "Integration Sensor", this); } | ||||
| void IntegrationSensor::process_sensor_value_(float value) { | ||||
|   if (std::isnan(value)) | ||||
|     return; | ||||
|   const uint32_t now = millis(); | ||||
|   const double old_value = this->last_value_; | ||||
|   const double new_value = value; | ||||
|   | ||||
| @@ -14,6 +14,9 @@ void MCP23008::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Read current output register state | ||||
|   this->read_reg(mcp23x08_base::MCP23X08_OLAT, &this->olat_); | ||||
|  | ||||
|   if (this->open_drain_ints_) { | ||||
|     // enable open-drain interrupt pins, 3.3V-safe | ||||
|     this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); | ||||
|   | ||||
| @@ -15,6 +15,10 @@ void MCP23016::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Read current output register state | ||||
|   this->read_reg_(MCP23016_OLAT0, &this->olat_0_); | ||||
|   this->read_reg_(MCP23016_OLAT1, &this->olat_1_); | ||||
|  | ||||
|   // all pins input | ||||
|   this->write_reg_(MCP23016_IODIR0, 0xFF); | ||||
|   this->write_reg_(MCP23016_IODIR1, 0xFF); | ||||
|   | ||||
| @@ -14,6 +14,10 @@ void MCP23017::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Read current output register state | ||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); | ||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); | ||||
|  | ||||
|   if (this->open_drain_ints_) { | ||||
|     // enable open-drain interrupt pins, 3.3V-safe | ||||
|     this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); | ||||
|   | ||||
| @@ -23,6 +23,9 @@ void MCP23S08::setup() { | ||||
|   this->transfer_byte(0b00011000);  // Enable HAEN pins for addressing | ||||
|   this->disable(); | ||||
|  | ||||
|   // Read current output register state | ||||
|   this->read_reg(mcp23x08_base::MCP23X08_OLAT, &this->olat_); | ||||
|  | ||||
|   if (this->open_drain_ints_) { | ||||
|     // enable open-drain interrupt pins, 3.3V-safe | ||||
|     this->write_reg(mcp23x08_base::MCP23X08_IOCON, 0x04); | ||||
|   | ||||
| @@ -23,6 +23,10 @@ void MCP23S17::setup() { | ||||
|   this->transfer_byte(0b00011000);  // Enable HAEN pins for addressing | ||||
|   this->disable(); | ||||
|  | ||||
|   // Read current output register state | ||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATA, &this->olat_a_); | ||||
|   this->read_reg(mcp23x17_base::MCP23X17_OLATB, &this->olat_b_); | ||||
|  | ||||
|   if (this->open_drain_ints_) { | ||||
|     // enable open-drain interrupt pins, 3.3V-safe | ||||
|     this->write_reg(mcp23x17_base::MCP23X17_IOCONA, 0x04); | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import spi | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.core import CORE | ||||
|  | ||||
| DEPENDENCIES = ["spi"] | ||||
| AUTO_LOAD = ["sensor"] | ||||
| @@ -24,6 +23,3 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await spi.register_spi_device(var, config) | ||||
|  | ||||
|     if CORE.is_esp32: | ||||
|         cg.add_library("SPI", None) | ||||
|   | ||||
							
								
								
									
										0
									
								
								esphome/components/mcp9600/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/mcp9600/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										115
									
								
								esphome/components/mcp9600/mcp9600.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								esphome/components/mcp9600/mcp9600.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| #include "mcp9600.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mcp9600 { | ||||
|  | ||||
| static const char *const TAG = "mcp9600"; | ||||
|  | ||||
| static const uint8_t MCP9600_REGISTER_HOT_JUNCTION = 0x00; | ||||
| // static const uint8_t MCP9600_REGISTER_JUNCTION_DELTA = 0x01; // Unused, but kept for future reference | ||||
| static const uint8_t MCP9600_REGISTER_COLD_JUNTION = 0x02; | ||||
| // static const uint8_t MCP9600_REGISTER_RAW_DATA_ADC = 0x03; // Unused, but kept for future reference | ||||
| static const uint8_t MCP9600_REGISTER_STATUS = 0x04; | ||||
| static const uint8_t MCP9600_REGISTER_SENSOR_CONFIG = 0x05; | ||||
| static const uint8_t MCP9600_REGISTER_CONFIG = 0x06; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT1_CONFIG = 0x08; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT2_CONFIG = 0x09; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT3_CONFIG = 0x0A; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT4_CONFIG = 0x0B; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT1_HYSTERESIS = 0x0C; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT2_HYSTERESIS = 0x0D; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT3_HYSTERESIS = 0x0E; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT4_HYSTERESIS = 0x0F; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT1_LIMIT = 0x10; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT2_LIMIT = 0x11; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT3_LIMIT = 0x12; | ||||
| static const uint8_t MCP9600_REGISTER_ALERT4_LIMIT = 0x13; | ||||
| static const uint8_t MCP9600_REGISTER_DEVICE_ID = 0x20; | ||||
|  | ||||
| void MCP9600Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up MCP9600..."); | ||||
|  | ||||
|   uint16_t dev_id = 0; | ||||
|   this->read_byte_16(MCP9600_REGISTER_DEVICE_ID, &dev_id); | ||||
|   this->device_id_ = (uint8_t)(dev_id >> 8); | ||||
|  | ||||
|   // Allows both MCP9600's and MCP9601's to be connected. | ||||
|   if (this->device_id_ != (uint8_t) 0x40 && this->device_id_ != (uint8_t) 0x41) { | ||||
|     this->error_code_ = COMMUNICATION_FAILED; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   bool success = this->write_byte(MCP9600_REGISTER_STATUS, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_SENSOR_CONFIG, uint8_t(0x00 | thermocouple_type_ << 4)); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_CONFIG, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT1_CONFIG, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT2_CONFIG, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT3_CONFIG, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT4_CONFIG, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT1_HYSTERESIS, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT2_HYSTERESIS, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT3_HYSTERESIS, 0x00); | ||||
|   success |= this->write_byte(MCP9600_REGISTER_ALERT4_HYSTERESIS, 0x00); | ||||
|   success |= this->write_byte_16(MCP9600_REGISTER_ALERT1_LIMIT, 0x0000); | ||||
|   success |= this->write_byte_16(MCP9600_REGISTER_ALERT2_LIMIT, 0x0000); | ||||
|   success |= this->write_byte_16(MCP9600_REGISTER_ALERT3_LIMIT, 0x0000); | ||||
|   success |= this->write_byte_16(MCP9600_REGISTER_ALERT4_LIMIT, 0x0000); | ||||
|  | ||||
|   if (!success) { | ||||
|     this->error_code_ = FAILED_TO_UPDATE_CONFIGURATION; | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MCP9600Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "MCP9600:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Device ID: 0x%x", this->device_id_); | ||||
|  | ||||
|   LOG_SENSOR("  ", "Hot Junction Temperature", this->hot_junction_sensor_); | ||||
|   LOG_SENSOR("  ", "Cold Junction Temperature", this->cold_junction_sensor_); | ||||
|  | ||||
|   switch (this->error_code_) { | ||||
|     case COMMUNICATION_FAILED: | ||||
|       ESP_LOGE(TAG, "Connected device does not match a known MCP9600 or MCP901 sensor"); | ||||
|       break; | ||||
|     case FAILED_TO_UPDATE_CONFIGURATION: | ||||
|       ESP_LOGE(TAG, "Failed to update device configuration"); | ||||
|       break; | ||||
|     case NONE: | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void MCP9600Component::update() { | ||||
|   if (this->hot_junction_sensor_ != nullptr) { | ||||
|     uint16_t raw_hot_junction_temperature; | ||||
|     if (!this->read_byte_16(MCP9600_REGISTER_HOT_JUNCTION, &raw_hot_junction_temperature)) { | ||||
|       this->status_set_warning(); | ||||
|       return; | ||||
|     } | ||||
|     float hot_junction_temperature = int16_t(raw_hot_junction_temperature) * 0.0625; | ||||
|     this->hot_junction_sensor_->publish_state(hot_junction_temperature); | ||||
|   } | ||||
|  | ||||
|   if (this->cold_junction_sensor_ != nullptr) { | ||||
|     uint16_t raw_cold_junction_temperature; | ||||
|     if (!this->read_byte_16(MCP9600_REGISTER_COLD_JUNTION, &raw_cold_junction_temperature)) { | ||||
|       this->status_set_warning(); | ||||
|       return; | ||||
|     } | ||||
|     float cold_junction_temperature = int16_t(raw_cold_junction_temperature) * 0.0625; | ||||
|     this->cold_junction_sensor_->publish_state(cold_junction_temperature); | ||||
|   } | ||||
|  | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| }  // namespace mcp9600 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										51
									
								
								esphome/components/mcp9600/mcp9600.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								esphome/components/mcp9600/mcp9600.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace mcp9600 { | ||||
|  | ||||
| enum MCP9600ThermocoupleType : uint8_t { | ||||
|   MCP9600_THERMOCOUPLE_TYPE_K = 0b000, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_J = 0b001, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_T = 0b010, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_N = 0b011, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_S = 0b100, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_E = 0b101, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_B = 0b110, | ||||
|   MCP9600_THERMOCOUPLE_TYPE_R = 0b111, | ||||
| }; | ||||
|  | ||||
| class MCP9600Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; } | ||||
|  | ||||
|   void set_hot_junction(sensor::Sensor *hot_junction) { this->hot_junction_sensor_ = hot_junction; } | ||||
|   void set_cold_junction(sensor::Sensor *cold_junction) { this->cold_junction_sensor_ = cold_junction; } | ||||
|   void set_thermocouple_type(MCP9600ThermocoupleType thermocouple_type) { | ||||
|     this->thermocouple_type_ = thermocouple_type; | ||||
|   }; | ||||
|  | ||||
|  protected: | ||||
|   uint8_t device_id_{0}; | ||||
|  | ||||
|   sensor::Sensor *hot_junction_sensor_{nullptr}; | ||||
|   sensor::Sensor *cold_junction_sensor_{nullptr}; | ||||
|  | ||||
|   MCP9600ThermocoupleType thermocouple_type_{MCP9600_THERMOCOUPLE_TYPE_K}; | ||||
|  | ||||
|   enum ErrorCode { | ||||
|     NONE, | ||||
|     COMMUNICATION_FAILED, | ||||
|     FAILED_TO_UPDATE_CONFIGURATION, | ||||
|   } error_code_{NONE}; | ||||
| }; | ||||
|  | ||||
| }  // namespace mcp9600 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										81
									
								
								esphome/components/mcp9600/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								esphome/components/mcp9600/sensor.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c, sensor | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
| ) | ||||
|  | ||||
| CONF_THERMOCOUPLE_TYPE = "thermocouple_type" | ||||
| CONF_HOT_JUNCTION = "hot_junction" | ||||
| CONF_COLD_JUNCTION = "cold_junction" | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
| CODEOWNERS = ["@MrEditor97"] | ||||
|  | ||||
| mcp9600_ns = cg.esphome_ns.namespace("mcp9600") | ||||
| MCP9600Component = mcp9600_ns.class_( | ||||
|     "MCP9600Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| MCP9600ThermocoupleType = mcp9600_ns.enum("MCP9600ThermocoupleType") | ||||
| THERMOCOUPLE_TYPE = { | ||||
|     "K": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_K, | ||||
|     "J": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_J, | ||||
|     "T": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_T, | ||||
|     "N": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_N, | ||||
|     "S": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_S, | ||||
|     "E": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_E, | ||||
|     "B": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_B, | ||||
|     "R": MCP9600ThermocoupleType.MCP9600_THERMOCOUPLE_TYPE_R, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(MCP9600Component), | ||||
|             cv.Optional(CONF_THERMOCOUPLE_TYPE, default="K"): cv.enum( | ||||
|                 THERMOCOUPLE_TYPE, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_HOT_JUNCTION): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|             cv.Optional(CONF_COLD_JUNCTION): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_CELSIUS, | ||||
|                 accuracy_decimals=1, | ||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x67)) | ||||
| ) | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema( | ||||
|     "mcp9600", min_frequency="100khz" | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     cg.add(var.set_thermocouple_type(config[CONF_THERMOCOUPLE_TYPE])) | ||||
|  | ||||
|     if CONF_HOT_JUNCTION in config: | ||||
|         conf = config[CONF_HOT_JUNCTION] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_hot_junction(sens)) | ||||
|  | ||||
|     if CONF_COLD_JUNCTION in config: | ||||
|         conf = config[CONF_COLD_JUNCTION] | ||||
|         sens = await sensor.new_sensor(conf) | ||||
|         cg.add(var.set_cold_junction(sens)) | ||||
| @@ -35,22 +35,6 @@ void Modbus::loop() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| uint16_t crc16(const uint8_t *data, uint8_t len) { | ||||
|   uint16_t crc = 0xFFFF; | ||||
|   while (len--) { | ||||
|     crc ^= *data++; | ||||
|     for (uint8_t i = 0; i < 8; i++) { | ||||
|       if ((crc & 0x01) != 0) { | ||||
|         crc >>= 1; | ||||
|         crc ^= 0xA001; | ||||
|       } else { | ||||
|         crc >>= 1; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   return crc; | ||||
| } | ||||
|  | ||||
| bool Modbus::parse_modbus_byte_(uint8_t byte) { | ||||
|   size_t at = this->rx_buffer_.size(); | ||||
|   this->rx_buffer_.push_back(byte); | ||||
|   | ||||
| @@ -40,8 +40,6 @@ class Modbus : public uart::UARTDevice, public Component { | ||||
|   std::vector<ModbusDevice *> devices_; | ||||
| }; | ||||
|  | ||||
| uint16_t crc16(const uint8_t *data, uint8_t len); | ||||
|  | ||||
| class ModbusDevice { | ||||
|  public: | ||||
|   void set_parent(Modbus *parent) { parent_ = parent; } | ||||
|   | ||||
| @@ -236,7 +236,7 @@ size_t ModbusController::create_register_ranges_() { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (curr->start_address == r.start_address) { | ||||
|     if (curr->start_address == r.start_address && curr->register_type == r.register_type) { | ||||
|       // use the lowest non zero value for the whole range | ||||
|       // Because zero is the default value for skip_updates it is excluded from getting the min value. | ||||
|       if (curr->skip_updates != 0) { | ||||
|   | ||||
| @@ -32,11 +32,11 @@ ModbusSwitch = modbus_controller_ns.class_( | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     switch.SWITCH_SCHEMA.extend(cv.COMPONENT_SCHEMA) | ||||
|     switch.switch_schema(ModbusSwitch) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(ModbusItemBaseSchema) | ||||
|     .extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ModbusSwitch), | ||||
|             cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE), | ||||
|             cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda, | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user