mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-01 07:31:51 +00:00 
			
		
		
		
	Compare commits
	
		
			239 Commits
		
	
	
		
			2022.5.0b3
			...
			2022.8.0b2
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | a67d58948d | ||
|  | 84c051d097 | ||
|  | b918abfd54 | ||
|  | 7f41b7cd93 | ||
|  | 4759b4fe2e | ||
|  | e2c8e69d12 | ||
|  | 917e8e155c | ||
|  | a97e3d827d | ||
|  | 029014d9d6 | ||
|  | e4c2922536 | ||
|  | 7133ae6aaa | ||
|  | 2f7f0ff3a1 | ||
|  | df853bf61e | ||
|  | f0ac753f9b | ||
|  | 4d56a975e6 | ||
|  | d56c53c848 | ||
|  | f83b16320d | ||
|  | ac10e27f08 | ||
|  | 34df7a6072 | ||
|  | e2cddf1005 | ||
|  | ced423748e | ||
|  | 77fb02729e | ||
|  | fef39b9fbe | ||
|  | 02810105fb | ||
|  | baad92515b | ||
|  | a12c6b5f35 | ||
|  | 522646c64d | ||
|  | 4791093e48 | ||
|  | 599a455150 | ||
|  | 2deef16ebe | ||
|  | 989b7be99b | ||
|  | cd473e1395 | ||
|  | e246ebfb2e | ||
|  | 8546ae56da | ||
|  | 9e227b0192 | ||
|  | ba7737e9f8 | ||
|  | 98aa3d51ed | ||
|  | e7cfb5492e | ||
|  | 9ed136dc3a | ||
|  | 9217216723 | ||
|  | 936c408a58 | ||
|  | 54427eac9a | ||
|  | e809488cc0 | ||
|  | ed26c57d99 | ||
|  | 2a49811f6e | ||
|  | c95acd2568 | ||
|  | 6a4e0cf667 | ||
|  | 04f4dd8a22 | ||
|  | f33d829ce9 | ||
|  | 578671ea94 | ||
|  | d10300c330 | ||
|  | e0555e140f | ||
|  | 093989406f | ||
|  | cdb16f08f6 | ||
|  | 53139c293b | ||
|  | 6a8bdcc315 | ||
|  | fe535939a3 | ||
|  | 09e6c11d73 | ||
|  | 8112bdfaa8 | ||
|  | f7db9aaa9f | ||
|  | 435f972357 | ||
|  | f82b46c16b | ||
|  | bca96f91b2 | ||
|  | 6f83a49c63 | ||
|  | 72cce391ab | ||
|  | 28d2949ebe | ||
|  | c4a0015997 | ||
|  | f564be6aea | ||
|  | 988f15e6af | ||
|  | 37b6d442bd | ||
|  | fb2467f6f0 | ||
|  | 29045b0435 | ||
|  | 311a48c64e | ||
|  | 01b3815f27 | ||
|  | b0d1c801bd | ||
|  | 5aaac06f5b | ||
|  | 34adbf0588 | ||
|  | 0a4213182e | ||
|  | b0c0258e70 | ||
|  | 8110e591d0 | ||
|  | fe05d7aec1 | ||
|  | 57f5884070 | ||
|  | f329c74a15 | ||
|  | 7c86f3fa9e | ||
|  | 203b8b01bf | ||
|  | 8a1034a92f | ||
|  | aa0c2dedd9 | ||
|  | d045908e05 | ||
|  | f002a23d2d | ||
|  | 29d6d0a906 | ||
|  | c8b58b5c23 | ||
|  | 01bfafc5f1 | ||
|  | 8c9948bb56 | ||
|  | 2d1abaa68e | ||
|  | 664a3df0b4 | ||
|  | 9ff893881c | ||
|  | 94f6c6861a | ||
|  | b1d614e6c4 | ||
|  | 7fceb070e5 | ||
|  | 06440d0202 | ||
|  | 0ecf9f4f2f | ||
|  | 5c7c0834c0 | ||
|  | f3a25de11d | ||
|  | 041bef8bcd | ||
|  | 8998c5f6dd | ||
|  | 6e83790308 | ||
|  | d2d4eb4eae | ||
|  | 5942a3898c | ||
|  | 93421f0fa7 | ||
|  | 3a9ab50dd2 | ||
|  | 5abd91d6d5 | ||
|  | c3da42516b | ||
|  | 6cb5cd48c2 | ||
|  | ec1fae6883 | ||
|  | 746fd1122f | ||
|  | 9663760ec5 | ||
|  | a3d73d1e23 | ||
|  | d63e14a4b6 | ||
|  | 03944e6cd8 | ||
|  | 0d1028be2e | ||
|  | 6a85259e4d | ||
|  | ebca936b7e | ||
|  | 31c4551890 | ||
|  | dd470d4197 | ||
|  | 612822490b | ||
|  | f8969605e8 | ||
|  | dd24ffa24e | ||
|  | d0dda48932 | ||
|  | 6349b5f654 | ||
|  | a6ff02a3cf | ||
|  | 4f57bf786b | ||
|  | 6221f6d47d | ||
|  | a922efeafa | ||
|  | 5aa42e5e66 | ||
|  | 708672ec7e | ||
|  | d2cefbf224 | ||
|  | adb7aa6950 | ||
|  | 77f322166e | ||
|  | f3f6e54818 | ||
|  | fb0fec1f25 | ||
|  | b66af9fb4d | ||
|  | 6617d576a7 | ||
|  | cd35ead890 | ||
|  | 9dc804ee27 | ||
|  | a8ceeaa7b0 | ||
|  | 7092f7663e | ||
|  | d9d2edeb08 | ||
|  | dda1ddcb26 | ||
|  | f0c890f160 | ||
|  | 4f52d43347 | ||
|  | 0ed7db979b | ||
|  | 9c78049359 | ||
|  | 7882105661 | ||
|  | c000e1d6dd | ||
|  | 420dacb22d | ||
|  | ae2f6ad4d1 | ||
|  | 2c28d79bf8 | ||
|  | c5069edc78 | ||
|  | 282d9e138c | ||
|  | 72fcf2cbe1 | ||
|  | 6f49f5465b | ||
|  | 17b8bd8316 | ||
|  | 9b6b9c1fa2 | ||
|  | 609a2ca592 | ||
|  | 6dabf24bf3 | ||
|  | 93e2506279 | ||
|  | f62d5d3b9d | ||
|  | 0665acd190 | ||
|  | fea05e9d33 | ||
|  | 7a03c7d56f | ||
|  | 2dc2aec954 | ||
|  | 39c6c2417a | ||
|  | 03d5a0ec1d | ||
|  | 1c873e0034 | ||
|  | bcb47c306c | ||
|  | 01c4d3c225 | ||
|  | c2aaae4818 | ||
|  | 3f678e218d | ||
|  | f8a1bd4e79 | ||
|  | 993044c870 | ||
|  | a8c1b63edb | ||
|  | db7d946e1b | ||
|  | fc7348d46d | ||
|  | 8be2456c7e | ||
|  | bb5f7249a6 | ||
|  | fc94a5d0ee | ||
|  | 24029cc918 | ||
|  | 9a9d5964ee | ||
|  | 4e4a512107 | ||
|  | 0729ed538e | ||
|  | 24b75b7ed6 | ||
|  | ec3618ecb8 | ||
|  | 792a24f38d | ||
|  | 652e8a015b | ||
|  | 1ef6fd8fb0 | ||
|  | 942b0de7fd | ||
|  | 859cca49d1 | ||
|  | 8f7ff25624 | ||
|  | 97aca8e54c | ||
|  | 95acf19067 | ||
|  | 3d0899aa58 | ||
|  | 138d6e505b | ||
|  | 2748e6ba29 | ||
|  | dbd4e927d8 | ||
|  | e73d47918f | ||
|  | b881bc071e | ||
|  | 1d0395d1c7 | ||
|  | 616c787e37 | ||
|  | 0c4de2bc97 | ||
|  | c2f5ac9eba | ||
|  | 5764c988af | ||
|  | ccc2fbfd67 | ||
|  | 10b4adb8e6 | ||
|  | 83b7181bcb | ||
|  | 8886b7e141 | ||
|  | 7dcc4d030b | ||
|  | b9398897c1 | ||
|  | 657b1c60ae | ||
|  | dc54b17778 | ||
|  | 1fb214165b | ||
|  | 81b2fd78f5 | ||
|  | 69002fb1e6 | ||
|  | 75332a752d | ||
|  | 09ed1aed93 | ||
|  | 53d3718028 | ||
|  | 2b5dce5232 | ||
|  | 9ad84150aa | ||
|  | c0523590b4 | ||
|  | c7f091ab10 | ||
|  | 7479e0aada | ||
|  | 5bbee1a1fe | ||
|  | bdb9546ca3 | ||
|  | 46af4cad6e | ||
|  | 76a238912b | ||
|  | 909a526967 | ||
|  | cd6f4fb93f | ||
|  | c19458696e | ||
|  | 318b930e9f | ||
|  | 9296a078a7 | 
							
								
								
									
										5
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.github/dependabot.yml
									
									
									
									
										vendored
									
									
								
							| @@ -7,3 +7,8 @@ updates: | ||||
|     ignore: | ||||
|       # Hypotehsis is only used for testing and is updated quite often | ||||
|       - dependency-name: hypothesis | ||||
|   - package-ecosystem: "github-actions" | ||||
|     directory: "/" | ||||
|     schedule: | ||||
|       interval: daily | ||||
|     open-pull-requests-limit: 10 | ||||
|   | ||||
							
								
								
									
										8
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -30,15 +30,15 @@ jobs: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|       uses: docker/setup-buildx-action@v2 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|       uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Set TAG | ||||
|       run: | | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -75,15 +75,15 @@ jobs: | ||||
|             pio_cache_key: tidyesp32-idf | ||||
|  | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v2 | ||||
|         uses: actions/setup-python@v4 | ||||
|         id: python | ||||
|         with: | ||||
|           python-version: '3.8' | ||||
|  | ||||
|       - name: Cache virtualenv | ||||
|         uses: actions/cache@v2 | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: .venv | ||||
|           key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} | ||||
| @@ -102,7 +102,7 @@ jobs: | ||||
|  | ||||
|       # Use per check platformio cache because checks use different parts | ||||
|       - name: Cache platformio | ||||
|         uses: actions/cache@v2 | ||||
|         uses: actions/cache@v3 | ||||
|         with: | ||||
|           path: ~/.platformio | ||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||
| @@ -131,7 +131,7 @@ jobs: | ||||
|         if: matrix.id == 'ci-custom' | ||||
|  | ||||
|       - name: Lint Python | ||||
|         run: script/lint-python | ||||
|         run: script/lint-python -a | ||||
|         if: matrix.id == 'lint-python' | ||||
|  | ||||
|       - run: esphome compile ${{ matrix.file }} | ||||
| @@ -163,4 +163,4 @@ jobs: | ||||
|  | ||||
|       - name: Suggested changes | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format') | ||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') | ||||
|   | ||||
							
								
								
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										26
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -17,7 +17,7 @@ jobs: | ||||
|     outputs: | ||||
|       tag: ${{ steps.tag.outputs.tag }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Get tag | ||||
|         id: tag | ||||
|         run: | | ||||
| @@ -35,9 +35,9 @@ jobs: | ||||
|     if: github.repository == 'esphome/esphome' && github.event_name == 'release' | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v2 | ||||
|       - uses: actions/checkout@v3 | ||||
|       - name: Set up Python | ||||
|         uses: actions/setup-python@v1 | ||||
|         uses: actions/setup-python@v4 | ||||
|         with: | ||||
|           python-version: '3.x' | ||||
|       - name: Set up python environment | ||||
| @@ -65,24 +65,24 @@ jobs: | ||||
|         arch: [amd64, armv7, aarch64] | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|  | ||||
|     - name: Set up Docker Buildx | ||||
|       uses: docker/setup-buildx-action@v1 | ||||
|       uses: docker/setup-buildx-action@v2 | ||||
|     - name: Set up QEMU | ||||
|       uses: docker/setup-qemu-action@v1 | ||||
|       uses: docker/setup-qemu-action@v2 | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v1 | ||||
|       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@v1 | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
| @@ -108,9 +108,9 @@ jobs: | ||||
|       matrix: | ||||
|         build_type: ["ha-addon", "docker", "lint"] | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - uses: actions/checkout@v3 | ||||
|     - name: Set up Python | ||||
|       uses: actions/setup-python@v2 | ||||
|       uses: actions/setup-python@v4 | ||||
|       with: | ||||
|         python-version: '3.9' | ||||
|     - name: Enable experimental manifest support | ||||
| @@ -119,12 +119,12 @@ jobs: | ||||
|         echo "{\"experimental\": \"enabled\"}" > ~/.docker/config.json | ||||
|  | ||||
|     - name: Log in to docker hub | ||||
|       uses: docker/login-action@v1 | ||||
|       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@v1 | ||||
|       uses: docker/login-action@v2 | ||||
|       with: | ||||
|           registry: ghcr.io | ||||
|           username: ${{ github.actor }} | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|       - uses: actions/stale@v5 | ||||
|         with: | ||||
|           days-before-pr-stale: 90 | ||||
|           days-before-pr-close: 7 | ||||
| @@ -35,7 +35,7 @@ jobs: | ||||
|   close-issues: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: actions/stale@v4 | ||||
|       - uses: actions/stale@v5 | ||||
|         with: | ||||
|           days-before-pr-stale: -1 | ||||
|           days-before-pr-close: -1 | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
| # See https://pre-commit.com/hooks.html for more hooks | ||||
| repos: | ||||
|   - repo: https://github.com/ambv/black | ||||
|     rev: 22.3.0 | ||||
|     rev: 22.6.0 | ||||
|     hooks: | ||||
|     - id: black | ||||
|       args: | ||||
| @@ -26,7 +26,7 @@ repos: | ||||
|           - --branch=release | ||||
|           - --branch=beta | ||||
|   - repo: https://github.com/asottile/pyupgrade | ||||
|     rev: v2.31.1 | ||||
|     rev: v2.37.3 | ||||
|     hooks: | ||||
|       - id: pyupgrade | ||||
|         args: [--py38-plus] | ||||
|   | ||||
							
								
								
									
										10
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								CODEOWNERS
									
									
									
									
									
								
							| @@ -52,6 +52,7 @@ esphome/components/cs5460a/* @balrog-kun | ||||
| esphome/components/cse7761/* @berfenger | ||||
| esphome/components/ct_clamp/* @jesserockz | ||||
| esphome/components/current_based/* @djwmarcx | ||||
| esphome/components/dac7678/* @NickB1 | ||||
| esphome/components/daly_bms/* @s1lvi0 | ||||
| esphome/components/dashboard_import/* @esphome/core | ||||
| esphome/components/debug/* @OttoWinter | ||||
| @@ -72,6 +73,7 @@ esphome/components/esp8266/* @esphome/core | ||||
| esphome/components/exposure_notifications/* @OttoWinter | ||||
| esphome/components/ezo/* @ssieb | ||||
| esphome/components/fastled_base/* @OttoWinter | ||||
| esphome/components/feedback/* @ianchi | ||||
| esphome/components/fingerprint_grow/* @OnFreund @loongyh | ||||
| esphome/components/globals/* @esphome/core | ||||
| esphome/components/gpio/* @esphome/core | ||||
| @@ -88,6 +90,7 @@ esphome/components/honeywellabp/* @RubyBailey | ||||
| esphome/components/hrxl_maxsonar_wr/* @netmikey | ||||
| esphome/components/hydreon_rgxx/* @functionpointer | ||||
| esphome/components/i2c/* @esphome/core | ||||
| esphome/components/i2s_audio/* @jesserockz | ||||
| esphome/components/improv_serial/* @esphome/core | ||||
| esphome/components/ina260/* @MrEditor97 | ||||
| esphome/components/inkbird_ibsth1_mini/* @fkirill | ||||
| @@ -102,6 +105,7 @@ esphome/components/lilygo_t5_47/touchscreen/* @jesserockz | ||||
| esphome/components/lock/* @esphome/core | ||||
| esphome/components/logger/* @esphome/core | ||||
| esphome/components/ltr390/* @sjtrny | ||||
| esphome/components/max31865/* @DAVe3283 | ||||
| esphome/components/max44009/* @berfenger | ||||
| esphome/components/max7219digit/* @rspaargaren | ||||
| esphome/components/max9611/* @mckaymatthew | ||||
| @@ -119,6 +123,7 @@ esphome/components/mcp47a1/* @jesserockz | ||||
| esphome/components/mcp9808/* @k7hpn | ||||
| esphome/components/md5/* @esphome/core | ||||
| esphome/components/mdns/* @esphome/core | ||||
| esphome/components/media_player/* @jesserockz | ||||
| esphome/components/midea/* @dudanov | ||||
| esphome/components/midea_ir/* @dudanov | ||||
| esphome/components/mitsubishi/* @RubyBailey | ||||
| @@ -178,15 +183,18 @@ esphome/components/sen5x/* @martgras | ||||
| esphome/components/sensirion_common/* @martgras | ||||
| esphome/components/sensor/* @esphome/core | ||||
| esphome/components/sgp40/* @SenexCrenshaw | ||||
| esphome/components/sgp4x/* @SenexCrenshaw @martgras | ||||
| esphome/components/shelly_dimmer/* @edge90 @rnauber | ||||
| esphome/components/sht4x/* @sjtrny | ||||
| esphome/components/shutdown/* @esphome/core @jsuanet | ||||
| esphome/components/sim800l/* @glmnet | ||||
| esphome/components/sm2135/* @BoukeHaarsma23 | ||||
| esphome/components/sml/* @alengwenus | ||||
| esphome/components/smt100/* @piechade | ||||
| esphome/components/socket/* @esphome/core | ||||
| esphome/components/sonoff_d1/* @anatoly-savchenkov | ||||
| esphome/components/spi/* @esphome/core | ||||
| esphome/components/sprinkler/* @kbx81 | ||||
| esphome/components/sps30/* @martgras | ||||
| esphome/components/ssd1322_base/* @kbx81 | ||||
| esphome/components/ssd1322_spi/* @kbx81 | ||||
| @@ -222,6 +230,7 @@ esphome/components/tsl2591/* @wjcarpenter | ||||
| esphome/components/tuya/binary_sensor/* @jesserockz | ||||
| esphome/components/tuya/climate/* @jesserockz | ||||
| esphome/components/tuya/number/* @frankiboy1 | ||||
| esphome/components/tuya/select/* @bearpawmaxim | ||||
| esphome/components/tuya/sensor/* @jesserockz | ||||
| esphome/components/tuya/switch/* @jesserockz | ||||
| esphome/components/tuya/text_sensor/* @dentra | ||||
| @@ -231,6 +240,7 @@ esphome/components/version/* @esphome/core | ||||
| esphome/components/wake_on_lan/* @willwill2will54 | ||||
| esphome/components/web_server_base/* @OttoWinter | ||||
| esphome/components/whirlpool/* @glmnet | ||||
| esphome/components/whynter/* @aeonsablaze | ||||
| esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
|   | ||||
| @@ -46,12 +46,10 @@ RUN \ | ||||
|     # Ubuntu python3-pip is missing wheel | ||||
|     pip3 install --no-cache-dir \ | ||||
|         wheel==0.37.1 \ | ||||
|         platformio==5.2.5 \ | ||||
|         platformio==6.0.2 \ | ||||
|     # Change some platformio settings | ||||
|     && platformio settings set enable_telemetry No \ | ||||
|     && platformio settings set check_libraries_interval 1000000 \ | ||||
|     && platformio settings set check_platformio_interval 1000000 \ | ||||
|     && platformio settings set check_platforms_interval 1000000 \ | ||||
|     && mkdir -p /piolibs | ||||
|  | ||||
|  | ||||
| @@ -96,7 +94,7 @@ RUN \ | ||||
|     apt-get update \ | ||||
|     # Use pinned versions so that we get updates with build caching | ||||
|     && apt-get install -y --no-install-recommends \ | ||||
|         nginx-light=1.18.0-6.1 \ | ||||
|         nginx-light=1.18.0-6.1+deb11u2 \ | ||||
|     && rm -rf \ | ||||
|         /tmp/* \ | ||||
|         /var/{cache,log}/* \ | ||||
| @@ -136,7 +134,7 @@ RUN \ | ||||
|         clang-tidy-11=1:11.0.1-2 \ | ||||
|         patch=2.7.6-7 \ | ||||
|         software-properties-common=0.96.20.2-2.1 \ | ||||
|         nano=5.4-2 \ | ||||
|         nano=5.4-2+deb11u1 \ | ||||
|         build-essential=12.9 \ | ||||
|         python3-dev=3.9.2-3 \ | ||||
|     && rm -rf \ | ||||
|   | ||||
| @@ -12,7 +12,7 @@ from esphome.const import ( | ||||
|     CONF_TYPE_ID, | ||||
|     CONF_TIME, | ||||
| ) | ||||
| from esphome.jsonschema import jschema_extractor | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.util import Registry | ||||
|  | ||||
|  | ||||
| @@ -23,11 +23,10 @@ def maybe_simple_id(*validators): | ||||
| def maybe_conf(conf, *validators): | ||||
|     validator = cv.All(*validators) | ||||
|  | ||||
|     @jschema_extractor("maybe") | ||||
|     @schema_extractor("maybe") | ||||
|     def validate(value): | ||||
|         # pylint: disable=comparison-with-callable | ||||
|         if value == jschema_extractor: | ||||
|             return validator | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return (validator, conf) | ||||
|  | ||||
|         if isinstance(value, dict): | ||||
|             return validator(value) | ||||
| @@ -111,11 +110,9 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|         # This should only happen with invalid configs, but let's have a nice error message. | ||||
|         return [schema(value)] | ||||
|  | ||||
|     @jschema_extractor("automation") | ||||
|     @schema_extractor("automation") | ||||
|     def validator(value): | ||||
|         # hack to get the schema | ||||
|         # pylint: disable=comparison-with-callable | ||||
|         if value == jschema_extractor: | ||||
|         if value == SCHEMA_EXTRACT: | ||||
|             return schema | ||||
|  | ||||
|         value = validator_(value) | ||||
|   | ||||
| @@ -121,7 +121,11 @@ void IRAM_ATTR HOT AcDimmerDataStore::gpio_intr() { | ||||
|       // calculate time until enable in µs: (1.0-value)*cycle_time, but with integer arithmetic | ||||
|       // also take into account min_power | ||||
|       auto min_us = this->cycle_time_us * this->min_power / 1000; | ||||
|       this->enable_time_us = std::max((uint32_t) 1, ((65535 - this->value) * (this->cycle_time_us - min_us)) / 65535); | ||||
|       // calculate required value to provide a true RMS voltage output | ||||
|       this->enable_time_us = | ||||
|           std::max((uint32_t) 1, (uint32_t)((65535 - (acos(1 - (2 * this->value / 65535.0)) / 3.14159 * 65535)) * | ||||
|                                             (this->cycle_time_us - min_us)) / | ||||
|                                      65535); | ||||
|       if (this->method == DIM_METHOD_LEADING_PULSE) { | ||||
|         // Minimum pulse time should be enough for the triac to trigger when it is close to the ZC zone | ||||
|         // this is for brightness near 99% | ||||
|   | ||||
| @@ -15,10 +15,21 @@ namespace esphome { | ||||
| namespace adc { | ||||
|  | ||||
| static const char *const TAG = "adc"; | ||||
| // 13 bits for S3 / 12 bit for all other esp32 variants | ||||
| // create a const to avoid the repated cast to enum | ||||
|  | ||||
| // 13bit for S2, and 12bit for all other esp32 variants | ||||
| #ifdef USE_ESP32 | ||||
| static const adc_bits_width_t ADC_WIDTH_MAX_SOC_BITS = static_cast<adc_bits_width_t>(ADC_WIDTH_MAX - 1); | ||||
|  | ||||
| #ifndef SOC_ADC_RTC_MAX_BITWIDTH | ||||
| #if USE_ESP32_VARIANT_ESP32S2 | ||||
| static const int SOC_ADC_RTC_MAX_BITWIDTH = 13; | ||||
| #else | ||||
| static const int SOC_ADC_RTC_MAX_BITWIDTH = 12; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| static const int ADC_MAX = (1 << SOC_ADC_RTC_MAX_BITWIDTH) - 1;    // 4095 (12 bit) or 8191 (13 bit) | ||||
| static const int ADC_HALF = (1 << SOC_ADC_RTC_MAX_BITWIDTH) >> 1;  // 2048 (12 bit) or 4096 (13 bit) | ||||
| #endif | ||||
|  | ||||
| void ADCSensor::setup() { | ||||
| @@ -51,10 +62,6 @@ void ADCSensor::setup() { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // adc_gpio_init doesn't exist on ESP32-S2, ESP32-C3 or ESP32-H2 | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32H2) && !defined(USE_ESP32_VARIANT_ESP32S2) | ||||
|   adc_gpio_init(ADC_UNIT_1, (adc_channel_t) channel_); | ||||
| #endif | ||||
| #endif  // USE_ESP32 | ||||
| } | ||||
|  | ||||
| @@ -75,16 +82,16 @@ void ADCSensor::dump_config() { | ||||
|   } else { | ||||
|     switch (this->attenuation_) { | ||||
|       case ADC_ATTEN_DB_0: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db (max 1.1V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 0db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_2_5: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db (max 1.5V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 2.5db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_6: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db (max 2.2V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 6db"); | ||||
|         break; | ||||
|       case ADC_ATTEN_DB_11: | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db (max 3.9V)"); | ||||
|         ESP_LOGCONFIG(TAG, " Attenuation: 11db"); | ||||
|         break; | ||||
|       default:  // This is to satisfy the unused ADC_ATTEN_MAX | ||||
|         break; | ||||
| @@ -129,16 +136,16 @@ float ADCSensor::sample() { | ||||
|     return mv / 1000.0f; | ||||
|   } | ||||
|  | ||||
|   int raw11, raw6 = 4095, raw2 = 4095, raw0 = 4095; | ||||
|   int raw11, raw6 = ADC_MAX, raw2 = ADC_MAX, raw0 = ADC_MAX; | ||||
|   adc1_config_channel_atten(channel_, ADC_ATTEN_DB_11); | ||||
|   raw11 = adc1_get_raw(channel_); | ||||
|   if (raw11 < 4095) { | ||||
|   if (raw11 < ADC_MAX) { | ||||
|     adc1_config_channel_atten(channel_, ADC_ATTEN_DB_6); | ||||
|     raw6 = adc1_get_raw(channel_); | ||||
|     if (raw6 < 4095) { | ||||
|     if (raw6 < ADC_MAX) { | ||||
|       adc1_config_channel_atten(channel_, ADC_ATTEN_DB_2_5); | ||||
|       raw2 = adc1_get_raw(channel_); | ||||
|       if (raw2 < 4095) { | ||||
|       if (raw2 < ADC_MAX) { | ||||
|         adc1_config_channel_atten(channel_, ADC_ATTEN_DB_0); | ||||
|         raw0 = adc1_get_raw(channel_); | ||||
|       } | ||||
| @@ -154,15 +161,15 @@ float ADCSensor::sample() { | ||||
|   uint32_t mv2 = esp_adc_cal_raw_to_voltage(raw2, &cal_characteristics_[(int) ADC_ATTEN_DB_2_5]); | ||||
|   uint32_t mv0 = esp_adc_cal_raw_to_voltage(raw0, &cal_characteristics_[(int) ADC_ATTEN_DB_0]); | ||||
|  | ||||
|   // Contribution of each value, in range 0-2048 | ||||
|   uint32_t c11 = std::min(raw11, 2048); | ||||
|   uint32_t c6 = 2048 - std::abs(raw6 - 2048); | ||||
|   uint32_t c2 = 2048 - std::abs(raw2 - 2048); | ||||
|   uint32_t c0 = std::min(4095 - raw0, 2048); | ||||
|   // max theoretical csum value is 2048*4 = 8192 | ||||
|   // Contribution of each value, in range 0-2048 (12 bit ADC) or 0-4096 (13 bit ADC) | ||||
|   uint32_t c11 = std::min(raw11, ADC_HALF); | ||||
|   uint32_t c6 = ADC_HALF - std::abs(raw6 - ADC_HALF); | ||||
|   uint32_t c2 = ADC_HALF - std::abs(raw2 - ADC_HALF); | ||||
|   uint32_t c0 = std::min(ADC_MAX - raw0, ADC_HALF); | ||||
|   // max theoretical csum value is 4096*4 = 16384 | ||||
|   uint32_t csum = c11 + c6 + c2 + c0; | ||||
|  | ||||
|   // each mv is max 3900; so max value is 3900*2048*4, fits in unsigned | ||||
|   // each mv is max 3900; so max value is 3900*4096*4, fits in unsigned32 | ||||
|   uint32_t mv_scaled = (mv11 * c11) + (mv6 * c6) + (mv2 * c2) + (mv0 * c0); | ||||
|   return mv_scaled / (float) (csum * 1000U); | ||||
| } | ||||
|   | ||||
| @@ -40,6 +40,8 @@ class AddressableLightDisplay : public display::DisplayBuffer, public PollingCom | ||||
|   void setup() override; | ||||
|   void display(); | ||||
|  | ||||
|   display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; } | ||||
|  | ||||
|  protected: | ||||
|   int get_width_internal() override; | ||||
|   int get_height_internal() override; | ||||
|   | ||||
| @@ -92,7 +92,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_ | ||||
|       } | ||||
|       if (this->codec_->has_unit()) { | ||||
|         this->fahrenheit_ = (this->codec_->unit_ == 'f'); | ||||
|         ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celcius"); | ||||
|         ESP_LOGD(TAG, "Anova units is %s", this->fahrenheit_ ? "fahrenheit" : "celsius"); | ||||
|         this->current_request_++; | ||||
|       } | ||||
|       this->publish_state(); | ||||
|   | ||||
| @@ -42,6 +42,7 @@ service APIConnection { | ||||
|   rpc select_command (SelectCommandRequest) returns (void) {} | ||||
|   rpc button_command (ButtonCommandRequest) returns (void) {} | ||||
|   rpc lock_command (LockCommandRequest) returns (void) {} | ||||
|   rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -472,6 +473,7 @@ enum SensorStateClass { | ||||
|   STATE_CLASS_NONE = 0; | ||||
|   STATE_CLASS_MEASUREMENT = 1; | ||||
|   STATE_CLASS_TOTAL_INCREASING = 2; | ||||
|   STATE_CLASS_TOTAL = 3; | ||||
| } | ||||
|  | ||||
| enum SensorLastResetType { | ||||
| @@ -991,7 +993,7 @@ message ListEntitiesLockResponse { | ||||
|   bool supports_open = 9; | ||||
|   bool requires_code = 10; | ||||
|  | ||||
|   # Not yet implemented: | ||||
|   // Not yet implemented: | ||||
|   string code_format = 11; | ||||
| } | ||||
| message LockStateResponse { | ||||
| @@ -1010,7 +1012,7 @@ message LockCommandRequest { | ||||
|   fixed32 key = 1; | ||||
|   LockCommand command = 2; | ||||
|  | ||||
|   # Not yet implemented: | ||||
|   // Not yet implemented: | ||||
|   bool has_code = 3; | ||||
|   string code = 4; | ||||
| } | ||||
| @@ -1040,3 +1042,60 @@ message ButtonCommandRequest { | ||||
|   fixed32 key = 1; | ||||
| } | ||||
|  | ||||
| // ==================== MEDIA PLAYER ==================== | ||||
| enum MediaPlayerState { | ||||
|   MEDIA_PLAYER_STATE_NONE = 0; | ||||
|   MEDIA_PLAYER_STATE_IDLE = 1; | ||||
|   MEDIA_PLAYER_STATE_PLAYING = 2; | ||||
|   MEDIA_PLAYER_STATE_PAUSED = 3; | ||||
| } | ||||
| enum MediaPlayerCommand { | ||||
|   MEDIA_PLAYER_COMMAND_PLAY = 0; | ||||
|   MEDIA_PLAYER_COMMAND_PAUSE = 1; | ||||
|   MEDIA_PLAYER_COMMAND_STOP = 2; | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3; | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4; | ||||
| } | ||||
| message ListEntitiesMediaPlayerResponse { | ||||
|   option (id) = 63; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|  | ||||
|   string object_id = 1; | ||||
|   fixed32 key = 2; | ||||
|   string name = 3; | ||||
|   string unique_id = 4; | ||||
|  | ||||
|   string icon = 5; | ||||
|   bool disabled_by_default = 6; | ||||
|   EntityCategory entity_category = 7; | ||||
|  | ||||
|   bool supports_pause = 8; | ||||
| } | ||||
| message MediaPlayerStateResponse { | ||||
|   option (id) = 64; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|   option (no_delay) = true; | ||||
|   fixed32 key = 1; | ||||
|   MediaPlayerState state = 2; | ||||
|   float volume = 3; | ||||
|   bool muted = 4; | ||||
| } | ||||
| message MediaPlayerCommandRequest { | ||||
|   option (id) = 65; | ||||
|   option (source) = SOURCE_CLIENT; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|   option (no_delay) = true; | ||||
|  | ||||
|   fixed32 key = 1; | ||||
|  | ||||
|   bool has_command = 2; | ||||
|   MediaPlayerCommand command = 3; | ||||
|  | ||||
|   bool has_volume = 4; | ||||
|   float volume = 5; | ||||
|  | ||||
|   bool has_media_url = 6; | ||||
|   string media_url = 7; | ||||
| } | ||||
|   | ||||
| @@ -12,9 +12,6 @@ | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
| #include "esphome/components/homeassistant/time/homeassistant_time.h" | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| #include "esphome/components/fan/fan_helpers.h" | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace api { | ||||
| @@ -253,9 +250,6 @@ void APIConnection::cover_command(const CoverCommandRequest &msg) { | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_FAN | ||||
| // Shut-up about usage of deprecated speed_level_to_enum/speed_enum_to_level functions for a bit. | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
| bool APIConnection::send_fan_state(fan::Fan *fan) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
| @@ -268,7 +262,6 @@ bool APIConnection::send_fan_state(fan::Fan *fan) { | ||||
|     resp.oscillating = fan->oscillating; | ||||
|   if (traits.supports_speed()) { | ||||
|     resp.speed_level = fan->speed; | ||||
|     resp.speed = static_cast<enums::FanSpeed>(fan::speed_level_to_enum(fan->speed, traits.supported_speed_count())); | ||||
|   } | ||||
|   if (traits.supports_direction()) | ||||
|     resp.direction = static_cast<enums::FanDirection>(fan->direction); | ||||
| @@ -295,8 +288,6 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|   if (fan == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto traits = fan->get_traits(); | ||||
|  | ||||
|   auto call = fan->make_call(); | ||||
|   if (msg.has_state) | ||||
|     call.set_state(msg.state); | ||||
| @@ -305,14 +296,11 @@ void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
|   if (msg.has_speed_level) { | ||||
|     // Prefer level | ||||
|     call.set_speed(msg.speed_level); | ||||
|   } else if (msg.has_speed) { | ||||
|     call.set_speed(fan::speed_enum_to_level(static_cast<fan::FanSpeed>(msg.speed), traits.supported_speed_count())); | ||||
|   } | ||||
|   if (msg.has_direction) | ||||
|     call.set_direction(static_cast<fan::FanDirection>(msg.direction)); | ||||
|   call.perform(); | ||||
| } | ||||
| #pragma GCC diagnostic pop | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_LIGHT | ||||
| @@ -745,6 +733,52 @@ void APIConnection::lock_command(const LockCommandRequest &msg) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) { | ||||
|   if (!this->state_subscription_) | ||||
|     return false; | ||||
|  | ||||
|   MediaPlayerStateResponse resp{}; | ||||
|   resp.key = media_player->get_object_id_hash(); | ||||
|   resp.state = static_cast<enums::MediaPlayerState>(media_player->state); | ||||
|   resp.volume = media_player->volume; | ||||
|   resp.muted = media_player->is_muted(); | ||||
|   return this->send_media_player_state_response(resp); | ||||
| } | ||||
| bool APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { | ||||
|   ListEntitiesMediaPlayerResponse msg; | ||||
|   msg.key = media_player->get_object_id_hash(); | ||||
|   msg.object_id = media_player->get_object_id(); | ||||
|   msg.name = media_player->get_name(); | ||||
|   msg.unique_id = get_default_unique_id("media_player", media_player); | ||||
|   msg.icon = media_player->get_icon(); | ||||
|   msg.disabled_by_default = media_player->is_disabled_by_default(); | ||||
|   msg.entity_category = static_cast<enums::EntityCategory>(media_player->get_entity_category()); | ||||
|  | ||||
|   auto traits = media_player->get_traits(); | ||||
|   msg.supports_pause = traits.get_supports_pause(); | ||||
|  | ||||
|   return this->send_list_entities_media_player_response(msg); | ||||
| } | ||||
| void APIConnection::media_player_command(const MediaPlayerCommandRequest &msg) { | ||||
|   media_player::MediaPlayer *media_player = App.get_media_player_by_key(msg.key); | ||||
|   if (media_player == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto call = media_player->make_call(); | ||||
|   if (msg.has_command) { | ||||
|     call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command)); | ||||
|   } | ||||
|   if (msg.has_volume) { | ||||
|     call.set_volume(msg.volume); | ||||
|   } | ||||
|   if (msg.has_media_url) { | ||||
|     call.set_media_url(msg.media_url); | ||||
|   } | ||||
|   call.perform(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| void APIConnection::send_camera_state(std::shared_ptr<esp32_camera::CameraImage> image) { | ||||
|   if (!this->state_subscription_) | ||||
|   | ||||
| @@ -82,6 +82,11 @@ class APIConnection : public APIServerConnection { | ||||
|   bool send_lock_state(lock::Lock *a_lock, lock::LockState state); | ||||
|   bool send_lock_info(lock::Lock *a_lock); | ||||
|   void lock_command(const LockCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_media_player_state(media_player::MediaPlayer *media_player); | ||||
|   bool send_media_player_info(media_player::MediaPlayer *media_player); | ||||
|   void media_player_command(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
|   bool send_log_message(int level, const char *tag, const char *line); | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call) { | ||||
|   | ||||
| @@ -270,7 +270,7 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) { | ||||
|  * | ||||
|  * If the handshake is still active when this method returns and a read/write can't take place at | ||||
|  * the moment, returns WOULD_BLOCK. | ||||
|  * If an error occured, returns that error. Only returns OK if the transport is ready for data | ||||
|  * If an error occurred, returns that error. Only returns OK if the transport is ready for data | ||||
|  * traffic. | ||||
|  */ | ||||
| APIError APINoiseFrameHelper::state_action_() { | ||||
| @@ -586,7 +586,7 @@ APIError APINoiseFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) { | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } else if (sent == -1) { | ||||
|     // an error occured | ||||
|     // an error occurred | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|     return APIError::SOCKET_WRITE_FAILED; | ||||
| @@ -980,7 +980,7 @@ APIError APIPlaintextFrameHelper::write_raw_(const struct iovec *iov, int iovcnt | ||||
|     } | ||||
|     return APIError::OK; | ||||
|   } else if (sent == -1) { | ||||
|     // an error occured | ||||
|     // an error occurred | ||||
|     state_ = State::FAILED; | ||||
|     HELPER_LOG("Socket write failed with errno %d", errno); | ||||
|     return APIError::SOCKET_WRITE_FAILED; | ||||
|   | ||||
| @@ -108,6 +108,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens | ||||
|       return "STATE_CLASS_MEASUREMENT"; | ||||
|     case enums::STATE_CLASS_TOTAL_INCREASING: | ||||
|       return "STATE_CLASS_TOTAL_INCREASING"; | ||||
|     case enums::STATE_CLASS_TOTAL: | ||||
|       return "STATE_CLASS_TOTAL"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| @@ -308,6 +310,36 @@ template<> const char *proto_enum_to_string<enums::LockCommand>(enums::LockComma | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::MediaPlayerState value) { | ||||
|   switch (value) { | ||||
|     case enums::MEDIA_PLAYER_STATE_NONE: | ||||
|       return "MEDIA_PLAYER_STATE_NONE"; | ||||
|     case enums::MEDIA_PLAYER_STATE_IDLE: | ||||
|       return "MEDIA_PLAYER_STATE_IDLE"; | ||||
|     case enums::MEDIA_PLAYER_STATE_PLAYING: | ||||
|       return "MEDIA_PLAYER_STATE_PLAYING"; | ||||
|     case enums::MEDIA_PLAYER_STATE_PAUSED: | ||||
|       return "MEDIA_PLAYER_STATE_PAUSED"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::MediaPlayerCommand value) { | ||||
|   switch (value) { | ||||
|     case enums::MEDIA_PLAYER_COMMAND_PLAY: | ||||
|       return "MEDIA_PLAYER_COMMAND_PLAY"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_PAUSE: | ||||
|       return "MEDIA_PLAYER_COMMAND_PAUSE"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_STOP: | ||||
|       return "MEDIA_PLAYER_COMMAND_STOP"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_MUTE: | ||||
|       return "MEDIA_PLAYER_COMMAND_MUTE"; | ||||
|     case enums::MEDIA_PLAYER_COMMAND_UNMUTE: | ||||
|       return "MEDIA_PLAYER_COMMAND_UNMUTE"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| } | ||||
| bool HelloRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
| @@ -4574,6 +4606,254 @@ void ButtonCommandRequest::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool ListEntitiesMediaPlayerResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 6: { | ||||
|       this->disabled_by_default = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 7: { | ||||
|       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 8: { | ||||
|       this->supports_pause = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesMediaPlayerResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->object_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->name = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->unique_id = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->icon = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool ListEntitiesMediaPlayerResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(1, this->object_id); | ||||
|   buffer.encode_fixed32(2, this->key); | ||||
|   buffer.encode_string(3, this->name); | ||||
|   buffer.encode_string(4, this->unique_id); | ||||
|   buffer.encode_string(5, this->icon); | ||||
|   buffer.encode_bool(6, this->disabled_by_default); | ||||
|   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||
|   buffer.encode_bool(8, this->supports_pause); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("ListEntitiesMediaPlayerResponse {\n"); | ||||
|   out.append("  object_id: "); | ||||
|   out.append("'").append(this->object_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  name: "); | ||||
|   out.append("'").append(this->name).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  unique_id: "); | ||||
|   out.append("'").append(this->unique_id).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  icon: "); | ||||
|   out.append("'").append(this->icon).append("'"); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  disabled_by_default: "); | ||||
|   out.append(YESNO(this->disabled_by_default)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  entity_category: "); | ||||
|   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  supports_pause: "); | ||||
|   out.append(YESNO(this->supports_pause)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool MediaPlayerStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->state = value.as_enum<enums::MediaPlayerState>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->muted = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->volume = value.as_float(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void MediaPlayerStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_enum<enums::MediaPlayerState>(2, this->state); | ||||
|   buffer.encode_float(3, this->volume); | ||||
|   buffer.encode_bool(4, this->muted); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void MediaPlayerStateResponse::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("MediaPlayerStateResponse {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  state: "); | ||||
|   out.append(proto_enum_to_string<enums::MediaPlayerState>(this->state)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  volume: "); | ||||
|   sprintf(buffer, "%g", this->volume); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  muted: "); | ||||
|   out.append(YESNO(this->muted)); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| bool MediaPlayerCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
|       this->has_command = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 3: { | ||||
|       this->command = value.as_enum<enums::MediaPlayerCommand>(); | ||||
|       return true; | ||||
|     } | ||||
|     case 4: { | ||||
|       this->has_volume = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     case 6: { | ||||
|       this->has_media_url = value.as_bool(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||
|   switch (field_id) { | ||||
|     case 7: { | ||||
|       this->media_url = value.as_string(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| bool MediaPlayerCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||
|   switch (field_id) { | ||||
|     case 1: { | ||||
|       this->key = value.as_fixed32(); | ||||
|       return true; | ||||
|     } | ||||
|     case 5: { | ||||
|       this->volume = value.as_float(); | ||||
|       return true; | ||||
|     } | ||||
|     default: | ||||
|       return false; | ||||
|   } | ||||
| } | ||||
| void MediaPlayerCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_fixed32(1, this->key); | ||||
|   buffer.encode_bool(2, this->has_command); | ||||
|   buffer.encode_enum<enums::MediaPlayerCommand>(3, this->command); | ||||
|   buffer.encode_bool(4, this->has_volume); | ||||
|   buffer.encode_float(5, this->volume); | ||||
|   buffer.encode_bool(6, this->has_media_url); | ||||
|   buffer.encode_string(7, this->media_url); | ||||
| } | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void MediaPlayerCommandRequest::dump_to(std::string &out) const { | ||||
|   __attribute__((unused)) char buffer[64]; | ||||
|   out.append("MediaPlayerCommandRequest {\n"); | ||||
|   out.append("  key: "); | ||||
|   sprintf(buffer, "%u", this->key); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_command: "); | ||||
|   out.append(YESNO(this->has_command)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  command: "); | ||||
|   out.append(proto_enum_to_string<enums::MediaPlayerCommand>(this->command)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_volume: "); | ||||
|   out.append(YESNO(this->has_volume)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  volume: "); | ||||
|   sprintf(buffer, "%g", this->volume); | ||||
|   out.append(buffer); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  has_media_url: "); | ||||
|   out.append(YESNO(this->has_media_url)); | ||||
|   out.append("\n"); | ||||
|  | ||||
|   out.append("  media_url: "); | ||||
|   out.append("'").append(this->media_url).append("'"); | ||||
|   out.append("\n"); | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -53,6 +53,7 @@ enum SensorStateClass : uint32_t { | ||||
|   STATE_CLASS_NONE = 0, | ||||
|   STATE_CLASS_MEASUREMENT = 1, | ||||
|   STATE_CLASS_TOTAL_INCREASING = 2, | ||||
|   STATE_CLASS_TOTAL = 3, | ||||
| }; | ||||
| enum SensorLastResetType : uint32_t { | ||||
|   LAST_RESET_NONE = 0, | ||||
| @@ -141,6 +142,19 @@ enum LockCommand : uint32_t { | ||||
|   LOCK_LOCK = 1, | ||||
|   LOCK_OPEN = 2, | ||||
| }; | ||||
| enum MediaPlayerState : uint32_t { | ||||
|   MEDIA_PLAYER_STATE_NONE = 0, | ||||
|   MEDIA_PLAYER_STATE_IDLE = 1, | ||||
|   MEDIA_PLAYER_STATE_PLAYING = 2, | ||||
|   MEDIA_PLAYER_STATE_PAUSED = 3, | ||||
| }; | ||||
| enum MediaPlayerCommand : uint32_t { | ||||
|   MEDIA_PLAYER_COMMAND_PLAY = 0, | ||||
|   MEDIA_PLAYER_COMMAND_PAUSE = 1, | ||||
|   MEDIA_PLAYER_COMMAND_STOP = 2, | ||||
|   MEDIA_PLAYER_COMMAND_MUTE = 3, | ||||
|   MEDIA_PLAYER_COMMAND_UNMUTE = 4, | ||||
| }; | ||||
|  | ||||
| }  // namespace enums | ||||
|  | ||||
| @@ -1146,6 +1160,60 @@ class ButtonCommandRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
|  public: | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_pause{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class MediaPlayerStateResponse : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   enums::MediaPlayerState state{}; | ||||
|   float volume{0.0f}; | ||||
|   bool muted{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class MediaPlayerCommandRequest : public ProtoMessage { | ||||
|  public: | ||||
|   uint32_t key{0}; | ||||
|   bool has_command{false}; | ||||
|   enums::MediaPlayerCommand command{}; | ||||
|   bool has_volume{false}; | ||||
|   float volume{0.0f}; | ||||
|   bool has_media_url{false}; | ||||
|   std::string media_url{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -310,6 +310,24 @@ bool APIServerConnectionBase::send_list_entities_button_response(const ListEntit | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIServerConnectionBase::send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_list_entities_media_player_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<ListEntitiesMediaPlayerResponse>(msg, 63); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool APIServerConnectionBase::send_media_player_state_response(const MediaPlayerStateResponse &msg) { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   ESP_LOGVV(TAG, "send_media_player_state_response: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|   return this->send_message_<MediaPlayerStateResponse>(msg, 64); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| #endif | ||||
| bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | ||||
|   switch (msg_type) { | ||||
|     case 1: { | ||||
| @@ -563,6 +581,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | ||||
|       ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_button_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case 65: { | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|       MediaPlayerCommandRequest msg; | ||||
|       msg.decode(msg_data, msg_size); | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|       ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str()); | ||||
| #endif | ||||
|       this->on_media_player_command_request(msg); | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
| @@ -813,6 +842,19 @@ void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) | ||||
|   this->lock_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->media_player_command(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -144,6 +144,15 @@ class APIServerConnectionBase : public ProtoService { | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
|   virtual void on_button_command_request(const ButtonCommandRequest &value){}; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_list_entities_media_player_response(const ListEntitiesMediaPlayerResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool send_media_player_state_response(const MediaPlayerStateResponse &msg); | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){}; | ||||
| #endif | ||||
|  protected: | ||||
|   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; | ||||
| @@ -192,6 +201,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   virtual void lock_command(const LockCommandRequest &msg) = 0; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0; | ||||
| #endif | ||||
|  protected: | ||||
|   void on_hello_request(const HelloRequest &msg) override; | ||||
| @@ -236,6 +248,9 @@ class APIServerConnection : public APIServerConnectionBase { | ||||
| #ifdef USE_LOCK | ||||
|   void on_lock_command_request(const LockCommandRequest &msg) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override; | ||||
| #endif | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
| @@ -272,6 +272,15 @@ void APIServer::on_lock_update(lock::Lock *obj) { | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| void APIServer::on_media_player_update(media_player::MediaPlayer *obj) { | ||||
|   if (obj->is_internal()) | ||||
|     return; | ||||
|   for (auto &c : this->clients_) | ||||
|     c->send_media_player_state(obj); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; } | ||||
| void APIServer::set_port(uint16_t port) { this->port_ = port; } | ||||
| APIServer *global_api_server = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -68,6 +68,9 @@ class APIServer : public Component, public Controller { | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   void on_lock_update(lock::Lock *obj) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||
| #endif | ||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||
|   | ||||
| @@ -64,5 +64,11 @@ bool ListEntitiesIterator::on_number(number::Number *number) { return this->clie | ||||
| bool ListEntitiesIterator::on_select(select::Select *select) { return this->client_->send_select_info(select); } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_info(media_player); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace api | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -51,6 +51,9 @@ class ListEntitiesIterator : public ComponentIterator { | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool on_lock(lock::Lock *a_lock) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||
| #endif | ||||
|   bool on_end() override; | ||||
|  | ||||
|   | ||||
| @@ -50,6 +50,11 @@ bool InitialStateIterator::on_select(select::Select *select) { | ||||
| #ifdef USE_LOCK | ||||
| bool InitialStateIterator::on_lock(lock::Lock *a_lock) { return this->client_->send_lock_state(a_lock, a_lock->state); } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_player) { | ||||
|   return this->client_->send_media_player_state(media_player); | ||||
| } | ||||
| #endif | ||||
| InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
| @@ -48,6 +48,9 @@ class InitialStateIterator : public ComponentIterator { | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
|   bool on_lock(lock::Lock *a_lock) override; | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||
| #endif | ||||
|  protected: | ||||
|   APIConnection *client_; | ||||
|   | ||||
| @@ -1 +1,52 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import ble_client, time | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_RECEIVE_TIMEOUT, | ||||
|     CONF_TIME_ID, | ||||
| ) | ||||
|  | ||||
| CODEOWNERS = ["@jhansche"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
| MULTI_CONF = True | ||||
| CONF_BEDJET_ID = "bedjet_id" | ||||
|  | ||||
| bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||
| BedJetHub = bedjet_ns.class_("BedJetHub", ble_client.BLEClientNode, cg.PollingComponent) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.COMPONENT_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(BedJetHub), | ||||
|             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||
|             cv.Optional( | ||||
|                 CONF_RECEIVE_TIMEOUT, default="0s" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("15s")) | ||||
| ) | ||||
|  | ||||
| BEDJET_CLIENT_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_BEDJET_ID): cv.use_id(BedJetHub), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def register_bedjet_child(var, config): | ||||
|     parent = await cg.get_variable(config[CONF_BEDJET_ID]) | ||||
|     cg.add(parent.register_child(var)) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|     if CONF_TIME_ID in config: | ||||
|         time_ = await cg.get_variable(config[CONF_TIME_ID]) | ||||
|         cg.add(var.set_time_id(time_)) | ||||
|     if CONF_RECEIVE_TIMEOUT in config: | ||||
|         cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) | ||||
|   | ||||
| @@ -1,644 +0,0 @@ | ||||
| #include "bedjet.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| using namespace esphome::climate; | ||||
|  | ||||
| /// Converts a BedJet temp step into degrees Celsius. | ||||
| float bedjet_temp_to_c(const uint8_t temp) { | ||||
|   // BedJet temp is "C*2"; to get C, divide by 2. | ||||
|   return temp / 2.0f; | ||||
| } | ||||
|  | ||||
| /// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. | ||||
| uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { | ||||
|   //  0 =  5% | ||||
|   // 19 = 100% | ||||
|   return 5 * fan + 5; | ||||
| } | ||||
|  | ||||
| static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { | ||||
|   if (fan_step >= 0 && fan_step <= 19) | ||||
|     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++) { | ||||
|     if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { | ||||
|       return i; | ||||
|     } | ||||
|   } | ||||
|   return -1; | ||||
| } | ||||
|  | ||||
| void Bedjet::upgrade_firmware() { | ||||
|   auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::dump_config() { | ||||
|   LOG_CLIMATE("", "BedJet Climate", this); | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported modes:"); | ||||
|   for (auto mode : traits.get_supported_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported fan modes:"); | ||||
|   for (const auto &mode : traits.get_supported_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); | ||||
|   } | ||||
|   for (const auto &mode : traits.get_supported_custom_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str()); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported presets:"); | ||||
|   for (auto preset : traits.get_supported_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|   } | ||||
|   for (const auto &preset : traits.get_supported_custom_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::setup() { | ||||
|   this->codec_ = make_unique<BedjetCodec>(); | ||||
|  | ||||
|   // restore set points | ||||
|   auto restore = this->restore_state_(); | ||||
|   if (restore.has_value()) { | ||||
|     ESP_LOGI(TAG, "Restored previous saved state."); | ||||
|     restore->apply(this); | ||||
|   } else { | ||||
|     // Initial status is unknown until we connect | ||||
|     this->reset_state_(); | ||||
|   } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   this->setup_time_(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| /** Resets states to defaults. */ | ||||
| void Bedjet::reset_state_() { | ||||
|   this->mode = climate::CLIMATE_MODE_OFF; | ||||
|   this->action = climate::CLIMATE_ACTION_IDLE; | ||||
|   this->target_temperature = NAN; | ||||
|   this->current_temperature = NAN; | ||||
|   this->preset.reset(); | ||||
|   this->custom_preset.reset(); | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void Bedjet::loop() {} | ||||
|  | ||||
| void Bedjet::control(const ClimateCall &call) { | ||||
|   ESP_LOGD(TAG, "Received Bedjet::control"); | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (call.get_mode().has_value()) { | ||||
|     ClimateMode mode = *call.get_mode(); | ||||
|     BedjetPacket *pkt; | ||||
|     switch (mode) { | ||||
|       case climate::CLIMATE_MODE_OFF: | ||||
|         pkt = this->codec_->get_button_request(BTN_OFF); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_HEAT: | ||||
|         pkt = this->codec_->get_button_request(BTN_HEAT); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_FAN_ONLY: | ||||
|         pkt = this->codec_->get_button_request(BTN_COOL); | ||||
|         break; | ||||
|       case climate::CLIMATE_MODE_DRY: | ||||
|         pkt = this->codec_->get_button_request(BTN_DRY); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGW(TAG, "Unsupported mode: %d", mode); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|       this->mode = mode; | ||||
|       // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     auto target_temp = *call.get_target_temperature(); | ||||
|     auto *pkt = this->codec_->get_set_target_temp_request(target_temp); | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->target_temperature = target_temp; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_preset().has_value()) { | ||||
|     ClimatePreset preset = *call.get_preset(); | ||||
|     BedjetPacket *pkt; | ||||
|  | ||||
|     if (preset == climate::CLIMATE_PRESET_BOOST) { | ||||
|       pkt = this->codec_->get_button_request(BTN_TURBO); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %d", preset); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->preset = preset; | ||||
|       this->custom_preset.reset(); | ||||
|       this->force_refresh_ = true; | ||||
|     } | ||||
|   } else if (call.get_custom_preset().has_value()) { | ||||
|     std::string preset = *call.get_custom_preset(); | ||||
|     BedjetPacket *pkt; | ||||
|  | ||||
|     if (preset == "M1") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M1); | ||||
|     } else if (preset == "M2") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M2); | ||||
|     } else if (preset == "M3") { | ||||
|       pkt = this->codec_->get_button_request(BTN_M3); | ||||
|     } else if (preset == "EXT HT") { | ||||
|       pkt = this->codec_->get_button_request(BTN_EXTHT); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|       this->custom_preset = preset; | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_fan_mode().has_value()) { | ||||
|     // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. | ||||
|     // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. | ||||
|     auto fan_mode = *call.get_fan_mode(); | ||||
|     BedjetPacket *pkt; | ||||
|     if (fan_mode == climate::CLIMATE_FAN_LOW) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(3 /* = 20% */); | ||||
|     } else if (fan_mode == climate::CLIMATE_FAN_MEDIUM) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(9 /* = 50% */); | ||||
|     } else if (fan_mode == climate::CLIMATE_FAN_HIGH) { | ||||
|       pkt = this->codec_->get_set_fan_speed_request(14 /* = 75% */); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), | ||||
|                LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|     } else { | ||||
|       this->force_refresh_ = true; | ||||
|     } | ||||
|   } else if (call.get_custom_fan_mode().has_value()) { | ||||
|     auto fan_mode = *call.get_custom_fan_mode(); | ||||
|     auto fan_step = bedjet_fan_speed_to_step(fan_mode); | ||||
|     if (fan_step >= 0 && fan_step <= 19) { | ||||
|       ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), | ||||
|                fan_step); | ||||
|       // The index should represent the fan_step index. | ||||
|       BedjetPacket *pkt = this->codec_->get_set_fan_speed_request(fan_step); | ||||
|       auto status = this->write_bedjet_packet_(pkt); | ||||
|       if (status) { | ||||
|         ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status); | ||||
|       } else { | ||||
|         this->force_refresh_ = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Bedjet::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); | ||||
|       this->status_set_warning(); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto *chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->char_handle_cmd_ = chr->handle; | ||||
|  | ||||
|       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->char_handle_status_ = chr->handle; | ||||
|       // We also need to obtain the config descriptor for this handle. | ||||
|       // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be | ||||
|       // able to look it up. | ||||
|       auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); | ||||
|       if (descr == nullptr) { | ||||
|         ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", | ||||
|                  this->char_handle_status_); | ||||
|       } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || | ||||
|                  descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { | ||||
|         ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, | ||||
|                  descr->uuid.to_string().c_str()); | ||||
|       } else { | ||||
|         this->config_descr_status_ = descr->handle; | ||||
|       } | ||||
|  | ||||
|       chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); | ||||
|       if (chr != nullptr) { | ||||
|         this->char_handle_name_ = chr->handle; | ||||
|         auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, | ||||
|                                               ESP_GATT_AUTH_REQ_NONE); | ||||
|         if (status) { | ||||
|           ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       ESP_LOGD(TAG, "Services complete: obtained char handles."); | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|  | ||||
|       this->set_notify_(true); | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|       if (this->time_id_.has_value()) { | ||||
|         this->send_local_time_(); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_DESCR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         // ESP_GATT_INVALID_ATTR_LEN | ||||
|         ESP_LOGW(TAG, "Error writing descr at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||
|         break; | ||||
|       } | ||||
|       // [16:44:44][V][bedjet:279]: [JOENJET] Register for notify event success: h=0x002a s=0 | ||||
|       // This might be the enable-notify descriptor? (or disable-notify) | ||||
|       ESP_LOGV(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, | ||||
|                param->write.status); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_CHAR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->write.handle == this->char_handle_cmd_) { | ||||
|         if (this->force_refresh_) { | ||||
|           // Command write was successful. Publish the pending state, hoping that notify will kick in. | ||||
|           this->publish_state(); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent_->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->read.handle == this->char_handle_status_) { | ||||
|         // This is the additional packet that doesn't fit in the notify packet. | ||||
|         this->codec_->decode_extra(param->read.value, param->read.value_len); | ||||
|       } else if (param->read.handle == this->char_handle_name_) { | ||||
|         // The data should represent the name. | ||||
|         if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { | ||||
|           std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len); | ||||
|           // this->set_name(bedjet_name); | ||||
|           ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       // This event means that ESP received the request to enable notifications on the client side. But we also have to | ||||
|       // tell the server that we want it to send notifications. Normally BLEClient parent would handle this | ||||
|       // automatically, but as soon as we set our status to Established, the parent is going to purge all the | ||||
|       // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable | ||||
|       // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write | ||||
|       // doesn't break anything. | ||||
|  | ||||
|       if (param->reg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(true); | ||||
|       this->last_notify_ = 0; | ||||
|       this->force_refresh_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||
|       // This event is not handled by the parent BLEClient, so we need to do this either way. | ||||
|       if (param->unreg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(false); | ||||
|       this->last_notify_ = 0; | ||||
|       // Now we wait until the next update() poll to re-register notify... | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (param->notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), | ||||
|                  this->char_handle_status_, param->notify.handle); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we | ||||
|       //  throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). | ||||
|       //  Another idea would be to keep notify off by default, and use update() as an opportunity to turn on | ||||
|       //  notify to get enough data to update status, then turn off notify again. | ||||
|  | ||||
|       uint32_t now = millis(); | ||||
|       auto delta = now - this->last_notify_; | ||||
|  | ||||
|       if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { | ||||
|         bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); | ||||
|         this->last_notify_ = now; | ||||
|  | ||||
|         if (needs_extra) { | ||||
|           // this means the packet was partial, so read the status characteristic to get the second part. | ||||
|           auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, | ||||
|                                                 this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) { | ||||
|             ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); | ||||
|           } | ||||
|         } | ||||
|  | ||||
|         if (this->force_refresh_) { | ||||
|           // If we requested an immediate update, do that now. | ||||
|           this->update(); | ||||
|           this->force_refresh_ = false; | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. | ||||
|  * | ||||
|  * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order | ||||
|  * to undo the same on unregister. It also allows us to maintain the config descriptor separately, | ||||
|  * since the parent BLEClient is going to purge all descriptors once we set our connection status | ||||
|  * to `Established`. | ||||
|  */ | ||||
| uint8_t Bedjet::write_notify_config_descriptor_(bool enable) { | ||||
|   auto handle = this->config_descr_status_; | ||||
|   if (handle == 0) { | ||||
|     ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); | ||||
|     return -1; | ||||
|   } | ||||
|  | ||||
|   // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. | ||||
|   uint8_t notify_en[] = {0, 0}; | ||||
|   notify_en[0] = enable; | ||||
|   auto status = | ||||
|       esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), | ||||
|                                      ¬ify_en[0], ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); | ||||
|     return status; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x", this->get_name().c_str(), enable ? "true" : "false", | ||||
|            handle); | ||||
|   return ESP_GATT_OK; | ||||
| } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
| /** Attempts to sync the local time (via `time_id`) to the BedJet device. */ | ||||
| void Bedjet::send_local_time_() { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   auto *time_id = *this->time_id_; | ||||
|   time::ESPTime now = time_id->now(); | ||||
|   if (now.is_valid()) { | ||||
|     uint8_t hour = now.hour; | ||||
|     uint8_t minute = now.minute; | ||||
|     BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); | ||||
|     auto status = this->write_bedjet_packet_(pkt); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Initializes time sync callbacks to support syncing current time to the BedJet. */ | ||||
| void Bedjet::setup_time_() { | ||||
|   if (this->time_id_.has_value()) { | ||||
|     this->send_local_time_(); | ||||
|     auto *time_id = *this->time_id_; | ||||
|     time_id->add_on_time_sync_callback([this] { this->send_local_time_(); }); | ||||
|     time::ESPTime now = time_id->now(); | ||||
|     ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| /** Writes one BedjetPacket to the BLE client on the BEDJET_COMMAND_UUID. */ | ||||
| uint8_t Bedjet::write_bedjet_packet_(BedjetPacket *pkt) { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     if (!this->parent_->enabled) { | ||||
|       ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); | ||||
|     } | ||||
|     return -1; | ||||
|   } | ||||
|   auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, | ||||
|                                          pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                          ESP_GATT_AUTH_REQ_NONE); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| /** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ | ||||
| uint8_t Bedjet::set_notify_(const bool enable) { | ||||
|   uint8_t status; | ||||
|   if (enable) { | ||||
|     status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } else { | ||||
|     status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                  this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| /** Attempts to update the climate device from the last received BedjetStatusPacket. | ||||
|  * | ||||
|  * @return `true` if the status has been applied; `false` if there is nothing to apply. | ||||
|  */ | ||||
| bool Bedjet::update_status_() { | ||||
|   if (!this->codec_->has_status()) | ||||
|     return false; | ||||
|  | ||||
|   BedjetStatusPacket status = *this->codec_->get_status_packet(); | ||||
|  | ||||
|   auto converted_temp = bedjet_temp_to_c(status.target_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->target_temperature = converted_temp; | ||||
|   converted_temp = bedjet_temp_to_c(status.ambient_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->current_temperature = converted_temp; | ||||
|  | ||||
|   const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(status.fan_step); | ||||
|   if (fan_mode_name != nullptr) { | ||||
|     this->custom_fan_mode = *fan_mode_name; | ||||
|   } | ||||
|  | ||||
|   // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. | ||||
|   switch (status.mode) { | ||||
|     case MODE_WAIT:  // Biorhythm "wait" step: device is idle | ||||
|     case MODE_STANDBY: | ||||
|       this->mode = climate::CLIMATE_MODE_OFF; | ||||
|       this->action = climate::CLIMATE_ACTION_IDLE; | ||||
|       this->fan_mode = climate::CLIMATE_FAN_OFF; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_HEAT: | ||||
|     case MODE_EXTHT: | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->action = climate::CLIMATE_ACTION_HEATING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_COOL: | ||||
|       this->mode = climate::CLIMATE_MODE_FAN_ONLY; | ||||
|       this->action = climate::CLIMATE_ACTION_COOLING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_DRY: | ||||
|       this->mode = climate::CLIMATE_MODE_DRY; | ||||
|       this->action = climate::CLIMATE_ACTION_DRYING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_TURBO: | ||||
|       this->preset = climate::CLIMATE_PRESET_BOOST; | ||||
|       this->custom_preset.reset(); | ||||
|       this->mode = climate::CLIMATE_MODE_HEAT; | ||||
|       this->action = climate::CLIMATE_ACTION_HEATING; | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), status.mode); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   if (this->is_valid_()) { | ||||
|     this->publish_state(); | ||||
|     this->codec_->clear_status(); | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|  | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| void Bedjet::update() { | ||||
|   ESP_LOGV(TAG, "[%s] update()", this->get_name().c_str()); | ||||
|  | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     if (!this->parent()->enabled) { | ||||
|       ESP_LOGD(TAG, "[%s] Not connected, because enabled=false", this->get_name().c_str()); | ||||
|     } else { | ||||
|       // Possibly still trying to connect. | ||||
|       ESP_LOGD(TAG, "[%s] Not connected, enabled=true", this->get_name().c_str()); | ||||
|     } | ||||
|  | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto result = this->update_status_(); | ||||
|   if (!result) { | ||||
|     uint32_t now = millis(); | ||||
|     uint32_t diff = now - this->last_notify_; | ||||
|  | ||||
|     if (this->last_notify_ == 0) { | ||||
|       // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. | ||||
|       // However, it could also mean that it's running, but failing to send notifications. | ||||
|       // We can try to unregister for notifications now, and then re-register, hoping to clear it up... | ||||
|       // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? | ||||
|  | ||||
|       ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); | ||||
|       this->set_notify_(false); | ||||
|     } else if (diff > NOTIFY_WARN_THRESHOLD) { | ||||
|       ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); | ||||
|     } | ||||
|  | ||||
|     if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); | ||||
|       this->parent()->set_enabled(false); | ||||
|       this->parent()->set_enabled(true); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										23
									
								
								esphome/components/bedjet/bedjet_child.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/bedjet/bedjet_child.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "bedjet_codec.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| // Forward declare BedJetHub | ||||
| class BedJetHub; | ||||
|  | ||||
| class BedJetClient : public Parented<BedJetHub> { | ||||
|  public: | ||||
|   virtual void on_status(const BedjetStatusPacket *data) = 0; | ||||
|   virtual void on_bedjet_state(bool is_ready) = 0; | ||||
|  | ||||
|  protected: | ||||
|   friend BedJetHub; | ||||
|   virtual std::string describe() = 0; | ||||
| }; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
							
								
								
									
										354
									
								
								esphome/components/bedjet/bedjet_climate.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								esphome/components/bedjet/bedjet_climate.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,354 @@ | ||||
| #include "bedjet_climate.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| using namespace esphome::climate; | ||||
|  | ||||
| /// Converts a BedJet temp step into degrees Celsius. | ||||
| float bedjet_temp_to_c(const uint8_t temp) { | ||||
|   // BedJet temp is "C*2"; to get C, divide by 2. | ||||
|   return temp / 2.0f; | ||||
| } | ||||
|  | ||||
| static const std::string *bedjet_fan_step_to_fan_mode(const uint8_t fan_step) { | ||||
|   if (fan_step <= 19) | ||||
|     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++) { | ||||
|     if (fan_step_percent == BEDJET_FAN_STEP_NAME_STRINGS[i]) { | ||||
|       return i; | ||||
|     } | ||||
|   } | ||||
|   return -1; | ||||
| } | ||||
|  | ||||
| static inline BedjetButton heat_button(BedjetHeatMode mode) { | ||||
|   return mode == HEAT_MODE_EXTENDED ? BTN_EXTHT : BTN_HEAT; | ||||
| } | ||||
|  | ||||
| std::string BedJetClimate::describe() { return "BedJet Climate"; } | ||||
|  | ||||
| void BedJetClimate::dump_config() { | ||||
|   LOG_CLIMATE("", "BedJet Climate", this); | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported modes:"); | ||||
|   for (auto mode : traits.get_supported_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_mode_to_string(mode))); | ||||
|   } | ||||
|   if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|     ESP_LOGCONFIG(TAG, "   - BedJet heating mode: EXT HT"); | ||||
|   } else { | ||||
|     ESP_LOGCONFIG(TAG, "   - BedJet heating mode: HEAT"); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported fan modes:"); | ||||
|   for (const auto &mode : traits.get_supported_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); | ||||
|   } | ||||
|   for (const auto &mode : traits.get_supported_custom_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str()); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported presets:"); | ||||
|   for (auto preset : traits.get_supported_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|   } | ||||
|   for (const auto &preset : traits.get_supported_custom_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetClimate::setup() { | ||||
|   // restore set points | ||||
|   auto restore = this->restore_state_(); | ||||
|   if (restore.has_value()) { | ||||
|     ESP_LOGI(TAG, "Restored previous saved state."); | ||||
|     restore->apply(this); | ||||
|   } else { | ||||
|     // Initial status is unknown until we connect | ||||
|     this->reset_state_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Resets states to defaults. */ | ||||
| void BedJetClimate::reset_state_() { | ||||
|   this->mode = CLIMATE_MODE_OFF; | ||||
|   this->action = CLIMATE_ACTION_IDLE; | ||||
|   this->target_temperature = NAN; | ||||
|   this->current_temperature = NAN; | ||||
|   this->preset.reset(); | ||||
|   this->custom_preset.reset(); | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void BedJetClimate::loop() {} | ||||
|  | ||||
| void BedJetClimate::control(const ClimateCall &call) { | ||||
|   ESP_LOGD(TAG, "Received BedJetClimate::control"); | ||||
|   if (!this->parent_->is_connected()) { | ||||
|     ESP_LOGW(TAG, "Not connected, cannot handle control call yet."); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (call.get_mode().has_value()) { | ||||
|     ClimateMode mode = *call.get_mode(); | ||||
|     bool button_result; | ||||
|     switch (mode) { | ||||
|       case CLIMATE_MODE_OFF: | ||||
|         button_result = this->parent_->button_off(); | ||||
|         break; | ||||
|       case CLIMATE_MODE_HEAT: | ||||
|         button_result = this->parent_->send_button(heat_button(this->heating_mode_)); | ||||
|         break; | ||||
|       case CLIMATE_MODE_FAN_ONLY: | ||||
|         button_result = this->parent_->button_cool(); | ||||
|         break; | ||||
|       case CLIMATE_MODE_DRY: | ||||
|         button_result = this->parent_->button_dry(); | ||||
|         break; | ||||
|       default: | ||||
|         ESP_LOGW(TAG, "Unsupported mode: %d", mode); | ||||
|         return; | ||||
|     } | ||||
|  | ||||
|     if (button_result) { | ||||
|       this->mode = mode; | ||||
|       // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_target_temperature().has_value()) { | ||||
|     auto target_temp = *call.get_target_temperature(); | ||||
|     auto result = this->parent_->set_target_temp(target_temp); | ||||
|  | ||||
|     if (result) { | ||||
|       this->target_temperature = target_temp; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_preset().has_value()) { | ||||
|     ClimatePreset preset = *call.get_preset(); | ||||
|     bool result; | ||||
|  | ||||
|     if (preset == CLIMATE_PRESET_BOOST) { | ||||
|       // We use BOOST preset for TURBO mode, which is a short-lived/high-heat mode. | ||||
|       result = this->parent_->button_turbo(); | ||||
|  | ||||
|       if (result) { | ||||
|         this->mode = CLIMATE_MODE_HEAT; | ||||
|         this->preset = CLIMATE_PRESET_BOOST; | ||||
|         this->custom_preset.reset(); | ||||
|       } | ||||
|     } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { | ||||
|       if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { | ||||
|         // We were in heat mode with Boost preset, and now preset is set to None, so revert to normal heat. | ||||
|         result = this->parent_->send_button(heat_button(this->heating_mode_)); | ||||
|         if (result) { | ||||
|           this->preset.reset(); | ||||
|           this->custom_preset.reset(); | ||||
|         } | ||||
|       } else { | ||||
|         ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", | ||||
|                  LOG_STR_ARG(climate_preset_to_string(preset)), LOG_STR_ARG(climate_mode_to_string(this->mode)), | ||||
|                  LOG_STR_ARG(climate_preset_to_string(this->preset.value_or(CLIMATE_PRESET_NONE)))); | ||||
|       } | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %d", preset); | ||||
|       return; | ||||
|     } | ||||
|   } else if (call.get_custom_preset().has_value()) { | ||||
|     std::string preset = *call.get_custom_preset(); | ||||
|     bool result; | ||||
|  | ||||
|     if (preset == "M1") { | ||||
|       result = this->parent_->button_memory1(); | ||||
|     } else if (preset == "M2") { | ||||
|       result = this->parent_->button_memory2(); | ||||
|     } else if (preset == "M3") { | ||||
|       result = this->parent_->button_memory3(); | ||||
|     } else if (preset == "LTD HT") { | ||||
|       result = this->parent_->button_heat(); | ||||
|     } else if (preset == "EXT HT") { | ||||
|       result = this->parent_->button_ext_heat(); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unsupported preset: %s", preset.c_str()); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (result) { | ||||
|       this->custom_preset = preset; | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (call.get_fan_mode().has_value()) { | ||||
|     // Climate fan mode only supports low/med/high, but the BedJet supports 5-100% increments. | ||||
|     // We can still support a ClimateCall that requests low/med/high, and just translate it to a step increment here. | ||||
|     auto fan_mode = *call.get_fan_mode(); | ||||
|     bool result; | ||||
|     if (fan_mode == CLIMATE_FAN_LOW) { | ||||
|       result = this->parent_->set_fan_speed(20); | ||||
|     } else if (fan_mode == CLIMATE_FAN_MEDIUM) { | ||||
|       result = this->parent_->set_fan_speed(50); | ||||
|     } else if (fan_mode == CLIMATE_FAN_HIGH) { | ||||
|       result = this->parent_->set_fan_speed(75); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Unsupported fan mode: %s", this->get_name().c_str(), | ||||
|                LOG_STR_ARG(climate_fan_mode_to_string(fan_mode))); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (result) { | ||||
|       this->fan_mode = fan_mode; | ||||
|       this->custom_fan_mode.reset(); | ||||
|     } | ||||
|   } else if (call.get_custom_fan_mode().has_value()) { | ||||
|     auto fan_mode = *call.get_custom_fan_mode(); | ||||
|     auto fan_index = bedjet_fan_speed_to_step(fan_mode); | ||||
|     if (fan_index <= 19) { | ||||
|       ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode.c_str(), | ||||
|                fan_index); | ||||
|       bool result = this->parent_->set_fan_index(fan_index); | ||||
|       if (result) { | ||||
|         this->custom_fan_mode = fan_mode; | ||||
|         this->fan_mode.reset(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetClimate::on_bedjet_state(bool is_ready) {} | ||||
|  | ||||
| void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|   ESP_LOGV(TAG, "[%s] Handling on_status with data=%p", this->get_name().c_str(), (void *) data); | ||||
|  | ||||
|   auto converted_temp = bedjet_temp_to_c(data->target_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->target_temperature = converted_temp; | ||||
|  | ||||
|   converted_temp = bedjet_temp_to_c(data->ambient_temp_step); | ||||
|   if (converted_temp > 0) | ||||
|     this->current_temperature = converted_temp; | ||||
|  | ||||
|   const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); | ||||
|   if (fan_mode_name != nullptr) { | ||||
|     this->custom_fan_mode = *fan_mode_name; | ||||
|   } | ||||
|  | ||||
|   // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. | ||||
|   switch (data->mode) { | ||||
|     case MODE_WAIT:  // Biorhythm "wait" step: device is idle | ||||
|     case MODE_STANDBY: | ||||
|       this->mode = CLIMATE_MODE_OFF; | ||||
|       this->action = CLIMATE_ACTION_IDLE; | ||||
|       this->fan_mode = CLIMATE_FAN_OFF; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_HEAT: | ||||
|       this->mode = CLIMATE_MODE_HEAT; | ||||
|       this->action = CLIMATE_ACTION_HEATING; | ||||
|       this->preset.reset(); | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->set_custom_preset_("LTD HT"); | ||||
|       } else { | ||||
|         this->custom_preset.reset(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case MODE_EXTHT: | ||||
|       this->mode = CLIMATE_MODE_HEAT; | ||||
|       this->action = CLIMATE_ACTION_HEATING; | ||||
|       this->preset.reset(); | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->custom_preset.reset(); | ||||
|       } else { | ||||
|         this->set_custom_preset_("EXT HT"); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case MODE_COOL: | ||||
|       this->mode = CLIMATE_MODE_FAN_ONLY; | ||||
|       this->action = CLIMATE_ACTION_COOLING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_DRY: | ||||
|       this->mode = CLIMATE_MODE_DRY; | ||||
|       this->action = CLIMATE_ACTION_DRYING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_TURBO: | ||||
|       this->preset = CLIMATE_PRESET_BOOST; | ||||
|       this->custom_preset.reset(); | ||||
|       this->mode = CLIMATE_MODE_HEAT; | ||||
|       this->action = CLIMATE_ACTION_HEATING; | ||||
|       break; | ||||
|  | ||||
|     default: | ||||
|       ESP_LOGW(TAG, "[%s] Unexpected mode: 0x%02X", this->get_name().c_str(), data->mode); | ||||
|       break; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGV(TAG, "[%s] After on_status, new mode=%s", this->get_name().c_str(), | ||||
|            LOG_STR_ARG(climate_mode_to_string(this->mode))); | ||||
|   // FIXME: compare new state to previous state. | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| /** Attempts to update the climate 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 BedJetClimate::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); | ||||
|  | ||||
|   if (this->is_valid_()) { | ||||
|     // TODO: only if state changed? | ||||
|     this->publish_state(); | ||||
|     this->status_clear_warning(); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| void BedJetClimate::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"); | ||||
| } | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,47 +1,33 @@ | ||||
| #pragma once | ||||
| 
 | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/components/climate/climate.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "bedjet_base.h" | ||||
| 
 | ||||
| #ifdef USE_TIME | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #endif | ||||
| #include "bedjet_child.h" | ||||
| #include "bedjet_codec.h" | ||||
| #include "bedjet_hub.h" | ||||
| 
 | ||||
| #ifdef USE_ESP32 | ||||
| 
 | ||||
| #include <esp_gattc_api.h> | ||||
| 
 | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
| 
 | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
| 
 | ||||
| static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); | ||||
| 
 | ||||
| class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||
| class BedJetClimate : public climate::Climate, public BedJetClient, public PollingComponent { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
| 
 | ||||
| #ifdef USE_TIME | ||||
|   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } | ||||
| #endif | ||||
|   void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||
|   /* BedJetClient status update */ | ||||
|   void on_status(const BedjetStatusPacket *data) override; | ||||
|   void on_bedjet_state(bool is_ready) override; | ||||
|   std::string describe() override; | ||||
| 
 | ||||
|   /** Attempts to check for and apply firmware updates. */ | ||||
|   void upgrade_firmware(); | ||||
|   /** Sets the default strategy to use for climate::CLIMATE_MODE_HEAT. */ | ||||
|   void set_heating_mode(BedjetHeatMode mode) { this->heating_mode_ = mode; } | ||||
| 
 | ||||
|   climate::ClimateTraits traits() override { | ||||
|     auto traits = climate::ClimateTraits(); | ||||
| @@ -73,6 +59,11 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod | ||||
|         "M2", | ||||
|         "M3", | ||||
|     }); | ||||
|     if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|       traits.add_supported_custom_preset("LTD HT"); | ||||
|     } else { | ||||
|       traits.add_supported_custom_preset("EXT HT"); | ||||
|     } | ||||
|     traits.set_visual_min_temperature(19.0); | ||||
|     traits.set_visual_max_temperature(43.0); | ||||
|     traits.set_visual_temperature_step(1.0); | ||||
| @@ -82,20 +73,8 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod | ||||
|  protected: | ||||
|   void control(const climate::ClimateCall &call) override; | ||||
| 
 | ||||
| #ifdef USE_TIME | ||||
|   void setup_time_(); | ||||
|   void send_local_time_(); | ||||
|   optional<time::RealTimeClock *> time_id_{}; | ||||
| #endif | ||||
|   BedjetHeatMode heating_mode_ = HEAT_MODE_HEAT; | ||||
| 
 | ||||
|   uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; | ||||
| 
 | ||||
|   static const uint32_t MIN_NOTIFY_THROTTLE = 5000; | ||||
|   static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; | ||||
|   static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; | ||||
| 
 | ||||
|   uint8_t set_notify_(bool enable); | ||||
|   uint8_t write_bedjet_packet_(BedjetPacket *pkt); | ||||
|   void reset_state_(); | ||||
|   bool update_status_(); | ||||
| 
 | ||||
| @@ -104,17 +83,6 @@ class Bedjet : public climate::Climate, public esphome::ble_client::BLEClientNod | ||||
|     return !std::isnan(this->current_temperature) && !std::isnan(this->target_temperature) && | ||||
|            this->current_temperature > 1 && this->target_temperature > 1; | ||||
|   } | ||||
| 
 | ||||
|   uint32_t last_notify_ = 0; | ||||
|   bool force_refresh_ = false; | ||||
| 
 | ||||
|   std::unique_ptr<BedjetCodec> codec_; | ||||
|   uint16_t char_handle_cmd_; | ||||
|   uint16_t char_handle_name_; | ||||
|   uint16_t char_handle_status_; | ||||
|   uint16_t config_descr_status_; | ||||
| 
 | ||||
|   uint8_t write_notify_config_descriptor_(bool enable); | ||||
| }; | ||||
| 
 | ||||
| }  // namespace bedjet
 | ||||
| @@ -1,4 +1,4 @@ | ||||
| #include "bedjet_base.h" | ||||
| #include "bedjet_codec.h" | ||||
| #include <cstdio> | ||||
| #include <cstring> | ||||
| 
 | ||||
| @@ -48,7 +48,16 @@ BedjetPacket *BedjetCodec::get_set_fan_speed_request(const uint8_t fan_step) { | ||||
| 
 | ||||
| /** Returns a BedjetPacket that will set the device's current time. */ | ||||
| BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_t minute) { | ||||
|   this->packet_.command = CMD_SET_TIME; | ||||
|   this->packet_.command = CMD_SET_CLOCK; | ||||
|   this->packet_.data_length = 2; | ||||
|   this->packet_.data[0] = hour; | ||||
|   this->packet_.data[1] = minute; | ||||
|   return this->clean_packet_(); | ||||
| } | ||||
| 
 | ||||
| /** Returns a BedjetPacket that will set the device's remaining runtime. */ | ||||
| BedjetPacket *BedjetCodec::get_set_runtime_remaining_request(const uint8_t hour, const uint8_t minute) { | ||||
|   this->packet_.command = CMD_SET_RUNTIME; | ||||
|   this->packet_.data_length = 2; | ||||
|   this->packet_.data[0] = hour; | ||||
|   this->packet_.data[1] = minute; | ||||
| @@ -57,17 +66,17 @@ BedjetPacket *BedjetCodec::get_set_time_request(const uint8_t hour, const uint8_ | ||||
| 
 | ||||
| /** Decodes the extra bytes that were received after being notified with a partial packet. */ | ||||
| void BedjetCodec::decode_extra(const uint8_t *data, uint16_t length) { | ||||
|   ESP_LOGV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||
|   ESP_LOGVV(TAG, "Received extra: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||
|   uint8_t offset = this->last_buffer_size_; | ||||
|   if (offset > 0 && length + offset <= sizeof(BedjetStatusPacket)) { | ||||
|     memcpy(((uint8_t *) (&this->buf_)) + offset, data, length); | ||||
|     ESP_LOGV(TAG, | ||||
|              "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " | ||||
|              "flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c, others=%02x>", | ||||
|              this->buf_._skip_1_, this->buf_._skip_2_, this->buf_._skip_3_, this->buf_.update_phase, | ||||
|              this->buf_.flags & 0x20 ? '1' : '0', this->buf_.flags & 0x10 ? '1' : '0', | ||||
|              this->buf_.flags & 0x04 ? '1' : '0', this->buf_.flags & 0x01 ? '1' : '0', | ||||
|              this->buf_.flags & ~(0x20 | 0x10 | 0x04 | 0x01)); | ||||
|     ESP_LOGVV(TAG, | ||||
|               "Extra bytes: skip1=0x%08x, skip2=0x%04x, skip3=0x%02x; update phase=0x%02x, " | ||||
|               "flags=BedjetFlags <conn=%c, leds=%c, units=%c, mute=%c; packed=%02x>", | ||||
|               this->buf_.unused_1, this->buf_.unused_2, this->buf_.unused_3, this->buf_.update_phase, | ||||
|               this->buf_.flags.conn_test_passed ? '1' : '0', this->buf_.flags.leds_enabled ? '1' : '0', | ||||
|               this->buf_.flags.units_setup ? '1' : '0', this->buf_.flags.beeps_muted ? '1' : '0', | ||||
|               this->buf_.flags_packed); | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "Could not determine where to append to, last offset=%d, max size=%u, new size would be %d", offset, | ||||
|              sizeof(BedjetStatusPacket), length + offset); | ||||
| @@ -82,8 +91,6 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { | ||||
|   ESP_LOGV(TAG, "Received: %d bytes: %d %d %d %d", length, data[1], data[2], data[3], data[4]); | ||||
| 
 | ||||
|   if (data[1] == PACKET_FORMAT_V3_HOME && data[3] == PACKET_TYPE_STATUS) { | ||||
|     this->status_packet_.reset(); | ||||
| 
 | ||||
|     // Clear old buffer
 | ||||
|     memset(&this->buf_, 0, sizeof(BedjetStatusPacket)); | ||||
|     // Copy new data into buffer
 | ||||
| @@ -91,23 +98,24 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { | ||||
|     this->last_buffer_size_ = length; | ||||
| 
 | ||||
|     // TODO: validate the packet checksum?
 | ||||
|     if (this->buf_.mode >= 0 && this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && | ||||
|         this->buf_.target_temp_step <= 86 && this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && | ||||
|         this->buf_.ambient_temp_step > 1 && this->buf_.ambient_temp_step <= 100) { | ||||
|     if (this->buf_.mode < 7 && this->buf_.target_temp_step >= 38 && this->buf_.target_temp_step <= 86 && | ||||
|         this->buf_.actual_temp_step > 1 && this->buf_.actual_temp_step <= 100 && this->buf_.ambient_temp_step > 1 && | ||||
|         this->buf_.ambient_temp_step <= 100) { | ||||
|       // and save it for the update() loop
 | ||||
|       this->status_packet_ = this->buf_; | ||||
|       return this->buf_.is_partial == 1; | ||||
|       this->status_packet_ = &this->buf_; | ||||
|       return this->buf_.is_partial; | ||||
|     } else { | ||||
|       this->status_packet_ = nullptr; | ||||
|       // TODO: log a warning if we detect that we connected to a non-V3 device.
 | ||||
|       ESP_LOGW(TAG, "Received potentially invalid packet (len %d):", length); | ||||
|     } | ||||
|   } else if (data[1] == PACKET_FORMAT_DEBUG || data[3] == PACKET_TYPE_DEBUG) { | ||||
|     // We don't actually know the packet format for this. Dump packets to log, in case a pattern presents itself.
 | ||||
|     ESP_LOGV(TAG, | ||||
|              "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF;  [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " | ||||
|              "[12]=%d, [-1]=%d", | ||||
|              bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], data[9], | ||||
|              data[10], data[11], data[12], data[length - 1]); | ||||
|     ESP_LOGVV(TAG, | ||||
|               "received DEBUG packet: set1=%01fF, set2=%01fF, air=%01fF;  [7]=%d, [8]=%d, [9]=%d, [10]=%d, [11]=%d, " | ||||
|               "[12]=%d, [-1]=%d", | ||||
|               bedjet_temp_to_f(data[4]), bedjet_temp_to_f(data[5]), bedjet_temp_to_f(data[6]), data[7], data[8], | ||||
|               data[9], data[10], data[11], data[12], data[length - 1]); | ||||
| 
 | ||||
|     if (this->has_status()) { | ||||
|       this->status_packet_->ambient_temp_step = data[6]; | ||||
| @@ -119,5 +127,35 @@ bool BedjetCodec::decode_notify(const uint8_t *data, uint16_t length) { | ||||
|   return false; | ||||
| } | ||||
| 
 | ||||
| /** @return `true` if the new packet is meaningfully different from the last seen packet. */ | ||||
| bool BedjetCodec::compare(const uint8_t *data, uint16_t length) { | ||||
|   if (data == nullptr) { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   if (length < 17) { | ||||
|     // New packet looks small, skip it.
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   if (this->buf_.packet_format != PACKET_FORMAT_V3_HOME || | ||||
|       this->buf_.packet_type != PACKET_TYPE_STATUS) {  // No last seen packet, so take the new one.
 | ||||
|     return true; | ||||
|   } | ||||
| 
 | ||||
|   if (data[1] != PACKET_FORMAT_V3_HOME || data[3] != PACKET_TYPE_STATUS) {  // New packet is not a v3 status, skip it.
 | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   // Now coerce it to a status packet and compare some key fields
 | ||||
|   const BedjetStatusPacket *test = reinterpret_cast<const BedjetStatusPacket *>(data); | ||||
|   // These are fields that will only change due to explicit action.
 | ||||
|   // That is why we do not check ambient or actual temp here, because those are environmental.
 | ||||
|   bool explicit_fields_changed = this->buf_.mode != test->mode || this->buf_.fan_step != test->fan_step || | ||||
|                                  this->buf_.target_temp_step != test->target_temp_step; | ||||
| 
 | ||||
|   return explicit_fields_changed; | ||||
| } | ||||
| 
 | ||||
| }  // namespace bedjet
 | ||||
| }  // namespace esphome
 | ||||
| @@ -14,18 +14,6 @@ struct BedjetPacket { | ||||
|   uint8_t data[2]; | ||||
| }; | ||||
| 
 | ||||
| struct BedjetFlags { | ||||
|   /* uint8_t */ | ||||
|   int a_ : 1;                // 0x80
 | ||||
|   int b_ : 1;                // 0x40
 | ||||
|   int conn_test_passed : 1;  ///< (0x20) Bit is set `1` if the last connection test passed.
 | ||||
|   int leds_enabled : 1;      ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
 | ||||
|   int c_ : 1;                // 0x08
 | ||||
|   int units_setup : 1;       ///< (0x04) Bit is set `1` if the device's units have been configured.
 | ||||
|   int d_ : 1;                // 0x02
 | ||||
|   int beeps_muted : 1;       ///< (0x01) Bit is set `1` if the device's sound output is muted.
 | ||||
| } __attribute__((packed)); | ||||
| 
 | ||||
| enum BedjetPacketFormat : uint8_t { | ||||
|   PACKET_FORMAT_DEBUG = 0x05,    //  5
 | ||||
|   PACKET_FORMAT_V3_HOME = 0x56,  // 86
 | ||||
| @@ -36,15 +24,25 @@ enum BedjetPacketType : uint8_t { | ||||
|   PACKET_TYPE_DEBUG = 0x2, | ||||
| }; | ||||
| 
 | ||||
| enum BedjetNotification : uint8_t { | ||||
|   NOTIFY_NONE = 0,                    ///< No notification pending
 | ||||
|   NOTIFY_FILTER = 1,                  ///< Clean Filter / Please check BedJet air filter and clean if necessary.
 | ||||
|   NOTIFY_UPDATE = 2,                  ///< Firmware Update / A newer version of firmware is available.
 | ||||
|   NOTIFY_UPDATE_FAIL = 3,             ///< Firmware Update / Unable to connect to the firmware update server.
 | ||||
|   NOTIFY_BIO_FAIL_CLOCK_NOT_SET = 4,  ///< The specified sequence cannot be run because the clock is not set
 | ||||
|   NOTIFY_BIO_FAIL_TOO_LONG = 5,  ///< The specified sequence cannot be run because it contains steps that would be too
 | ||||
|                                  ///< long running from the current time.
 | ||||
|   // Note: after handling a notification, send MAGIC_NOTIFY_ACK
 | ||||
| }; | ||||
| 
 | ||||
| /** The format of a BedJet V3 status packet. */ | ||||
| struct BedjetStatusPacket { | ||||
|   // [0]
 | ||||
|   uint8_t is_partial : 8;  ///< `1` indicates that this is a partial packet, and more data can be read directly from the
 | ||||
|                            ///< characteristic.
 | ||||
|   bool is_partial : 8;  ///< `1` indicates that this is a partial packet, and more data can be read directly from the
 | ||||
|                         ///< characteristic.
 | ||||
|   BedjetPacketFormat packet_format : 8;  ///< BedjetPacketFormat::PACKET_FORMAT_V3_HOME for BedJet V3 status packet
 | ||||
|                                          ///< format. BedjetPacketFormat::PACKET_FORMAT_DEBUG for debugging packets.
 | ||||
|   uint8_t | ||||
|       expecting_length : 8;  ///< The expected total length of the status packet after merging the additional packet.
 | ||||
|   uint8_t expecting_length : 8;      ///< The expected total length of the status packet after merging the extra packet.
 | ||||
|   BedjetPacketType packet_type : 8;  ///< Typically BedjetPacketType::PACKET_TYPE_STATUS for BedJet V3 status packet.
 | ||||
| 
 | ||||
|   // [4]
 | ||||
| @@ -77,11 +75,26 @@ struct BedjetStatusPacket { | ||||
|   uint8_t shutdown_reason : 8;    ///< The reason for the last device shutdown.
 | ||||
| 
 | ||||
|   // [19-25]; the initial partial packet cuts off here after [19]
 | ||||
|   // Skip 7 bytes?
 | ||||
|   uint32_t _skip_1_ : 32;  // Unknown 19-22 = 0x01810112
 | ||||
| 
 | ||||
|   uint16_t _skip_2_ : 16;  // Unknown 23-24 = 0x1310
 | ||||
|   uint8_t _skip_3_ : 8;    // Unknown 25 = 0x00
 | ||||
|   uint8_t unused_1 : 8;  // Unknown [19] = 0x01
 | ||||
|   uint8_t unused_2 : 8;  // Unknown [20] = 0x81
 | ||||
|   uint8_t unused_3 : 8;  // Unknown [21] = 0x01
 | ||||
| 
 | ||||
|   // [22]: 0x2=is_dual_zone, ...?
 | ||||
|   struct { | ||||
|     int unused_1 : 1;       // 0x80
 | ||||
|     int unused_2 : 1;       // 0x40
 | ||||
|     int unused_3 : 1;       // 0x20
 | ||||
|     int unused_4 : 1;       // 0x10
 | ||||
|     int unused_5 : 1;       // 0x8
 | ||||
|     int unused_6 : 1;       // 0x4
 | ||||
|     bool is_dual_zone : 1;  /// Is part of a Dual Zone configuration
 | ||||
|     int unused_7 : 1;       // 0x1
 | ||||
|   } dual_zone_flags; | ||||
| 
 | ||||
|   uint8_t unused_4 : 8;  // Unknown 23-24 = 0x1310
 | ||||
|   uint8_t unused_5 : 8;  // Unknown 23-24 = 0x1310
 | ||||
|   uint8_t unused_6 : 8;  // Unknown 25 = 0x00
 | ||||
| 
 | ||||
|   // [26]
 | ||||
|   //   0x18(24) = "Connection test has completed OK"
 | ||||
| @@ -89,10 +102,27 @@ struct BedjetStatusPacket { | ||||
|   uint8_t update_phase : 8;  ///< The current status/phase of a firmware update.
 | ||||
| 
 | ||||
|   // [27]
 | ||||
|   // FIXME: cannot nest packed struct of matching length here?
 | ||||
|   /* BedjetFlags */ uint8_t flags : 8;  /// See BedjetFlags for the packed byte flags.
 | ||||
|   // [28-31]; 20+11 bytes
 | ||||
|   uint32_t _skip_4_ : 32;  // Unknown
 | ||||
|   union { | ||||
|     uint8_t flags_packed; | ||||
|     struct { | ||||
|       /* uint8_t */ | ||||
|       int unused_1 : 1;           // 0x80
 | ||||
|       int unused_2 : 1;           // 0x40
 | ||||
|       bool conn_test_passed : 1;  ///< (0x20) Bit is set `1` if the last connection test passed.
 | ||||
|       bool leds_enabled : 1;      ///< (0x10) Bit is set `1` if the LEDs on the device are enabled.
 | ||||
|       int unused_3 : 1;           // 0x08
 | ||||
|       bool units_setup : 1;       ///< (0x04) Bit is set `1` if the device's units have been configured.
 | ||||
|       int unused_4 : 1;           // 0x02
 | ||||
|       bool beeps_muted : 1;       ///< (0x01) Bit is set `1` if the device's sound output is muted.
 | ||||
|     } __attribute__((packed)) flags; | ||||
|   }; | ||||
| 
 | ||||
|   // [28] = (biorhythm?) sequence step
 | ||||
|   uint8_t bio_sequence_step : 8;  /// Biorhythm sequence step number
 | ||||
|   // [29] = notify_code:
 | ||||
|   BedjetNotification notify_code : 8;  /// See BedjetNotification
 | ||||
| 
 | ||||
|   uint16_t unused_7 : 16;  // Unknown
 | ||||
| 
 | ||||
| } __attribute__((packed)); | ||||
| 
 | ||||
| @@ -127,7 +157,7 @@ struct BedjetStatusPacket { | ||||
|  * - Set current time | ||||
|  *   The BedJet needs to have its clock set properly in order to run the biorhythm programs, which might | ||||
|  *   contain time-of-day based step rules. | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_SET_TIME | ||||
|  *   - BedjetPacket#command = BedjetCommand::CMD_SET_CLOCK | ||||
|  *   - BedjetPacket#data [0] is hours, [1] is minutes | ||||
|  */ | ||||
| class BedjetCodec { | ||||
| @@ -136,13 +166,15 @@ class BedjetCodec { | ||||
|   BedjetPacket *get_set_target_temp_request(float temperature); | ||||
|   BedjetPacket *get_set_fan_speed_request(uint8_t fan_step); | ||||
|   BedjetPacket *get_set_time_request(uint8_t hour, uint8_t minute); | ||||
|   BedjetPacket *get_set_runtime_remaining_request(uint8_t hour, uint8_t minute); | ||||
| 
 | ||||
|   bool decode_notify(const uint8_t *data, uint16_t length); | ||||
|   void decode_extra(const uint8_t *data, uint16_t length); | ||||
|   bool compare(const uint8_t *data, uint16_t length); | ||||
| 
 | ||||
|   inline bool has_status() { return this->status_packet_.has_value(); } | ||||
|   const optional<BedjetStatusPacket> &get_status_packet() const { return this->status_packet_; } | ||||
|   void clear_status() { this->status_packet_.reset(); } | ||||
|   inline bool has_status() { return this->status_packet_ != nullptr; } | ||||
|   const BedjetStatusPacket *get_status_packet() const { return this->status_packet_; } | ||||
|   void clear_status() { this->status_packet_ = nullptr; } | ||||
| 
 | ||||
|  protected: | ||||
|   BedjetPacket *clean_packet_(); | ||||
| @@ -151,7 +183,7 @@ class BedjetCodec { | ||||
| 
 | ||||
|   BedjetPacket packet_; | ||||
| 
 | ||||
|   optional<BedjetStatusPacket> status_packet_; | ||||
|   BedjetStatusPacket *status_packet_; | ||||
|   BedjetStatusPacket buf_; | ||||
| }; | ||||
| 
 | ||||
| @@ -7,6 +7,14 @@ namespace bedjet { | ||||
|  | ||||
| static const char *const TAG = "bedjet"; | ||||
|  | ||||
| /// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%. | ||||
| inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) { | ||||
|   //  0 =  5% | ||||
|   // 19 = 100% | ||||
|   return 5 * fan + 5; | ||||
| } | ||||
| inline static uint8_t bedjet_fan_speed_to_index(const uint8_t speed) { return speed / 5 - 1; } | ||||
|  | ||||
| enum BedjetMode : uint8_t { | ||||
|   /// BedJet is Off | ||||
|   MODE_STANDBY = 0, | ||||
| @@ -24,6 +32,14 @@ enum BedjetMode : uint8_t { | ||||
|   MODE_WAIT = 6, | ||||
| }; | ||||
|  | ||||
| /** Optional heating strategies to use for climate::CLIMATE_MODE_HEAT. */ | ||||
| enum BedjetHeatMode { | ||||
|   /// HVACMode.HEAT is handled using BTN_HEAT (default) | ||||
|   HEAT_MODE_HEAT, | ||||
|   /// HVACMode.HEAT is handled using BTN_EXTHT | ||||
|   HEAT_MODE_EXTENDED, | ||||
| }; | ||||
|  | ||||
| enum BedjetButton : uint8_t { | ||||
|   /// Turn BedJet off | ||||
|   BTN_OFF = 0x1, | ||||
| @@ -54,20 +70,23 @@ enum BedjetButton : uint8_t { | ||||
|   MAGIC_CONNTEST = 0x42, | ||||
|   /// Request a firmware update. This will also restart the Bedjet. | ||||
|   MAGIC_UPDATE = 0x43, | ||||
|   /// Acknowledge notification handled. See BedjetNotify | ||||
|   MAGIC_NOTIFY_ACK = 0x52, | ||||
| }; | ||||
|  | ||||
| enum BedjetCommand : uint8_t { | ||||
|   CMD_BUTTON = 0x1, | ||||
|   CMD_SET_RUNTIME = 0x2, | ||||
|   CMD_SET_TEMP = 0x3, | ||||
|   CMD_STATUS = 0x6, | ||||
|   CMD_SET_FAN = 0x7, | ||||
|   CMD_SET_TIME = 0x8, | ||||
|   CMD_SET_CLOCK = 0x8, | ||||
| }; | ||||
|  | ||||
| #define BEDJET_FAN_STEP_NAMES_ \ | ||||
|   { \ | ||||
|     "  5%", " 10%", " 15%", " 20%", " 25%", " 30%", " 35%", " 40%", " 45%", " 50%", " 55%", " 60%", " 65%", " 70%", \ | ||||
|         " 75%", " 80%", " 85%", " 90%", " 95%", "100%" \ | ||||
|     "5%", "10%", "15%", "20%", "25%", "30%", "35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", \ | ||||
|         "85%", "90%", "95%", "100%" \ | ||||
|   } | ||||
|  | ||||
| static const char *const BEDJET_FAN_STEP_NAMES[20] = BEDJET_FAN_STEP_NAMES_; | ||||
|   | ||||
							
								
								
									
										559
									
								
								esphome/components/bedjet/bedjet_hub.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										559
									
								
								esphome/components/bedjet/bedjet_hub.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,559 @@ | ||||
| #include "bedjet_hub.h" | ||||
| #include "bedjet_child.h" | ||||
| #include "bedjet_const.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| static const LogString *bedjet_button_to_string(BedjetButton button) { | ||||
|   switch (button) { | ||||
|     case BTN_OFF: | ||||
|       return LOG_STR("OFF"); | ||||
|     case BTN_COOL: | ||||
|       return LOG_STR("COOL"); | ||||
|     case BTN_HEAT: | ||||
|       return LOG_STR("HEAT"); | ||||
|     case BTN_EXTHT: | ||||
|       return LOG_STR("EXT HT"); | ||||
|     case BTN_TURBO: | ||||
|       return LOG_STR("TURBO"); | ||||
|     case BTN_DRY: | ||||
|       return LOG_STR("DRY"); | ||||
|     case BTN_M1: | ||||
|       return LOG_STR("M1"); | ||||
|     case BTN_M2: | ||||
|       return LOG_STR("M2"); | ||||
|     case BTN_M3: | ||||
|       return LOG_STR("M3"); | ||||
|     default: | ||||
|       return LOG_STR("unknown"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* Public */ | ||||
|  | ||||
| void BedJetHub::upgrade_firmware() { | ||||
|   auto *pkt = this->codec_->get_button_request(MAGIC_UPDATE); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] MAGIC_UPDATE button failed, status=%d", this->get_name().c_str(), status); | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool BedJetHub::button_heat() { return this->send_button(BTN_HEAT); } | ||||
| bool BedJetHub::button_ext_heat() { return this->send_button(BTN_EXTHT); } | ||||
| bool BedJetHub::button_turbo() { return this->send_button(BTN_TURBO); } | ||||
| bool BedJetHub::button_cool() { return this->send_button(BTN_COOL); } | ||||
| bool BedJetHub::button_dry() { return this->send_button(BTN_DRY); } | ||||
| bool BedJetHub::button_off() { return this->send_button(BTN_OFF); } | ||||
| bool BedJetHub::button_memory1() { return this->send_button(BTN_M1); } | ||||
| bool BedJetHub::button_memory2() { return this->send_button(BTN_M2); } | ||||
| bool BedJetHub::button_memory3() { return this->send_button(BTN_M3); } | ||||
|  | ||||
| bool BedJetHub::set_fan_index(uint8_t fan_speed_index) { | ||||
|   if (fan_speed_index > 19) { | ||||
|     ESP_LOGW(TAG, "Invalid fan speed index %d, expecting 0-19.", fan_speed_index); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   auto *pkt = this->codec_->get_set_fan_speed_request(fan_speed_index); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] writing fan speed failed, status=%d", this->get_name().c_str(), status); | ||||
|   } | ||||
|   return status == 0; | ||||
| } | ||||
|  | ||||
| uint8_t BedJetHub::get_fan_index() { | ||||
|   auto *status = this->codec_->get_status_packet(); | ||||
|   if (status != nullptr) { | ||||
|     return status->fan_step; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| bool BedJetHub::set_target_temp(float temp_c) { | ||||
|   auto *pkt = this->codec_->get_set_target_temp_request(temp_c); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] writing target temp failed, status=%d", this->get_name().c_str(), status); | ||||
|   } | ||||
|   return status == 0; | ||||
| } | ||||
|  | ||||
| bool BedJetHub::set_time_remaining(uint8_t hours, uint8_t mins) { | ||||
|   // FIXME: this may fail depending on current mode or other restrictions enforced by the unit. | ||||
|   auto *pkt = this->codec_->get_set_runtime_remaining_request(hours, mins); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] writing remaining runtime failed, status=%d", this->get_name().c_str(), status); | ||||
|   } | ||||
|   return status == 0; | ||||
| } | ||||
|  | ||||
| bool BedJetHub::send_button(BedjetButton button) { | ||||
|   auto *pkt = this->codec_->get_button_request(button); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|  | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "[%s] writing button %s failed, status=%d", this->get_name().c_str(), | ||||
|              LOG_STR_ARG(bedjet_button_to_string(button)), status); | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "[%s] writing button %s success", this->get_name().c_str(), | ||||
|              LOG_STR_ARG(bedjet_button_to_string(button))); | ||||
|   } | ||||
|   return status == 0; | ||||
| } | ||||
|  | ||||
| uint16_t BedJetHub::get_time_remaining() { | ||||
|   auto *status = this->codec_->get_status_packet(); | ||||
|   if (status != nullptr) { | ||||
|     return status->time_remaining_secs + status->time_remaining_mins * 60 + status->time_remaining_hrs * 3600; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
| /* Bluetooth/GATT */ | ||||
|  | ||||
| uint8_t BedJetHub::write_bedjet_packet_(BedjetPacket *pkt) { | ||||
|   if (!this->is_connected()) { | ||||
|     if (!this->parent_->enabled) { | ||||
|       ESP_LOGI(TAG, "[%s] Cannot write packet: Not connected, enabled=false", this->get_name().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "[%s] Cannot write packet: Not connected", this->get_name().c_str()); | ||||
|     } | ||||
|     return -1; | ||||
|   } | ||||
|   auto status = esp_ble_gattc_write_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_cmd_, | ||||
|                                          pkt->data_length + 1, (uint8_t *) &pkt->command, ESP_GATT_WRITE_TYPE_NO_RSP, | ||||
|                                          ESP_GATT_AUTH_REQ_NONE); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| /** Configures the local ESP BLE client to register (`true`) or unregister (`false`) for status notifications. */ | ||||
| uint8_t BedJetHub::set_notify_(const bool enable) { | ||||
|   uint8_t status; | ||||
|   if (enable) { | ||||
|     status = esp_ble_gattc_register_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_register_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } else { | ||||
|     status = esp_ble_gattc_unregister_for_notify(this->parent_->gattc_if, this->parent_->remote_bda, | ||||
|                                                  this->char_handle_status_); | ||||
|     if (status) { | ||||
|       ESP_LOGW(TAG, "[%s] esp_ble_gattc_unregister_for_notify failed, status=%d", this->get_name().c_str(), status); | ||||
|     } | ||||
|   } | ||||
|   ESP_LOGV(TAG, "[%s] set_notify: enable=%d; result=%d", this->get_name().c_str(), enable, status); | ||||
|   return status; | ||||
| } | ||||
|  | ||||
| bool BedJetHub::discover_characteristics_() { | ||||
|   bool result = true; | ||||
|   esphome::ble_client::BLECharacteristic *chr; | ||||
|  | ||||
|   if (!this->char_handle_cmd_) { | ||||
|     chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_COMMAND_UUID); | ||||
|     if (chr == nullptr) { | ||||
|       ESP_LOGW(TAG, "[%s] No control service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|       result = false; | ||||
|     } else { | ||||
|       this->char_handle_cmd_ = chr->handle; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!this->char_handle_status_) { | ||||
|     chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_STATUS_UUID); | ||||
|     if (chr == nullptr) { | ||||
|       ESP_LOGW(TAG, "[%s] No status service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|       result = false; | ||||
|     } else { | ||||
|       this->char_handle_status_ = chr->handle; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!this->config_descr_status_) { | ||||
|     // We also need to obtain the config descriptor for this handle. | ||||
|     // Otherwise once we set node_state=Established, the parent will flush all handles/descriptors, and we won't be | ||||
|     // able to look it up. | ||||
|     auto *descr = this->parent_->get_config_descriptor(this->char_handle_status_); | ||||
|     if (descr == nullptr) { | ||||
|       ESP_LOGW(TAG, "No config descriptor for status handle 0x%x. Will not be able to receive status notifications", | ||||
|                this->char_handle_status_); | ||||
|       result = false; | ||||
|     } else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 || | ||||
|                descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) { | ||||
|       ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_, | ||||
|                descr->uuid.to_string().c_str()); | ||||
|       result = false; | ||||
|     } else { | ||||
|       this->config_descr_status_ = descr->handle; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!this->char_handle_name_) { | ||||
|     chr = this->parent_->get_characteristic(BEDJET_SERVICE_UUID, BEDJET_NAME_UUID); | ||||
|     if (chr == nullptr) { | ||||
|       ESP_LOGW(TAG, "[%s] No name service found at device, not a BedJet..?", this->get_name().c_str()); | ||||
|       result = false; | ||||
|     } else { | ||||
|       this->char_handle_name_ = chr->handle; | ||||
|       auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, this->char_handle_name_, | ||||
|                                             ESP_GATT_AUTH_REQ_NONE); | ||||
|       if (status) { | ||||
|         ESP_LOGI(TAG, "[%s] Unable to read name characteristic: %d", this->get_name().c_str(), status); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str()); | ||||
|   ESP_LOGI(TAG, "     - Command char: 0x%x", this->char_handle_cmd_); | ||||
|   ESP_LOGI(TAG, "     - Status char: 0x%x", this->char_handle_status_); | ||||
|   ESP_LOGI(TAG, "       - config descriptor: 0x%x", this->config_descr_status_); | ||||
|   ESP_LOGI(TAG, "     - Name char: 0x%x", this->char_handle_name_); | ||||
|  | ||||
|   return result; | ||||
| } | ||||
|  | ||||
| void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                     esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_DISCONNECT_EVT: { | ||||
|       ESP_LOGV(TAG, "Disconnected: reason=%d", param->disconnect.reason); | ||||
|       this->status_set_warning(); | ||||
|       this->dispatch_state_(false); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       // FIXME: bug in BLEClient | ||||
|       this->parent_->conn_id = param->open.conn_id; | ||||
|       this->open_conn_id_ = param->open.conn_id; | ||||
|       break; | ||||
|     } | ||||
|  | ||||
|     case ESP_GATTC_CONNECT_EVT: { | ||||
|       if (this->parent_->conn_id != param->connect.conn_id && this->open_conn_id_ != 0xff) { | ||||
|         // FIXME: bug in BLEClient | ||||
|         ESP_LOGW(TAG, "[%s] CONNECT_EVT unexpected conn_id; open=%d, parent=%d, param=%d", this->get_name().c_str(), | ||||
|                  this->open_conn_id_, this->parent_->conn_id, param->connect.conn_id); | ||||
|         this->parent_->conn_id = this->open_conn_id_; | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto result = this->discover_characteristics_(); | ||||
|  | ||||
|       if (result) { | ||||
|         ESP_LOGD(TAG, "[%s] Services complete: obtained char handles.", this->get_name().c_str()); | ||||
|         this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|         this->set_notify_(true); | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|         if (this->time_id_.has_value()) { | ||||
|           this->send_local_time(); | ||||
|         } | ||||
| #endif | ||||
|  | ||||
|         this->dispatch_state_(true); | ||||
|       } else { | ||||
|         ESP_LOGW(TAG, "[%s] Failed discovering service characteristics.", this->get_name().c_str()); | ||||
|         this->parent()->set_enabled(false); | ||||
|         this->status_set_warning(); | ||||
|         this->dispatch_state_(false); | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_DESCR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         if (param->write.status == ESP_GATT_INVALID_ATTR_LEN) { | ||||
|           // This probably means that our hack for notify_en (8 bit vs 16 bit) didn't work right. | ||||
|           // Should we try to fall back to BLEClient's way? | ||||
|           ESP_LOGW(TAG, "[%s] Invalid attr length writing descr at handle 0x%04d, status=%d", this->get_name().c_str(), | ||||
|                    param->write.handle, param->write.status); | ||||
|         } else { | ||||
|           ESP_LOGW(TAG, "[%s] Error writing descr at handle 0x%04d, status=%d", this->get_name().c_str(), | ||||
|                    param->write.handle, param->write.status); | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       ESP_LOGD(TAG, "[%s] Write to handle 0x%04x status=%d", this->get_name().c_str(), param->write.handle, | ||||
|                param->write.status); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_WRITE_CHAR_EVT: { | ||||
|       if (param->write.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error writing char at handle 0x%04d, status=%d", param->write.handle, param->write.status); | ||||
|         break; | ||||
|       } | ||||
|       if (param->write.handle == this->char_handle_cmd_) { | ||||
|         if (this->force_refresh_) { | ||||
|           // Command write was successful. Publish the pending state, hoping that notify will kick in. | ||||
|           // FIXME: better to wait until we know the status has changed | ||||
|           this->dispatch_status_(); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_READ_CHAR_EVT: { | ||||
|       if (param->read.conn_id != this->parent_->conn_id) | ||||
|         break; | ||||
|       if (param->read.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       if (param->read.handle == this->char_handle_status_) { | ||||
|         // This is the additional packet that doesn't fit in the notify packet. | ||||
|         this->codec_->decode_extra(param->read.value, param->read.value_len); | ||||
|         this->status_packet_ready_(); | ||||
|       } else if (param->read.handle == this->char_handle_name_) { | ||||
|         // The data should represent the name. | ||||
|         if (param->read.status == ESP_GATT_OK && param->read.value_len > 0) { | ||||
|           std::string bedjet_name(reinterpret_cast<char const *>(param->read.value), param->read.value_len); | ||||
|           ESP_LOGV(TAG, "[%s] Got BedJet name: '%s'", this->get_name().c_str(), bedjet_name.c_str()); | ||||
|           this->set_name_(bedjet_name); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||
|       // This event means that ESP received the request to enable notifications on the client side. But we also have to | ||||
|       // tell the server that we want it to send notifications. Normally BLEClient parent would handle this | ||||
|       // automatically, but as soon as we set our status to Established, the parent is going to purge all the | ||||
|       // service/char/descriptor handles, and then get_config_descriptor() won't work anymore. There's no way to disable | ||||
|       // the BLEClient parent behavior, so our only option is to write the handle anyway, and hope a double-write | ||||
|       // doesn't break anything. | ||||
|  | ||||
|       if (param->reg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Register for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->reg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(true); | ||||
|       this->last_notify_ = 0; | ||||
|       this->force_refresh_ = true; | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||
|       // This event is not handled by the parent BLEClient, so we need to do this either way. | ||||
|       if (param->unreg_for_notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unregister for notify on unexpected handle 0x%04x, expecting 0x%04x", | ||||
|                  this->get_name().c_str(), param->unreg_for_notify.handle, this->char_handle_status_); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       this->write_notify_config_descriptor_(false); | ||||
|       this->last_notify_ = 0; | ||||
|       // Now we wait until the next update() poll to re-register notify... | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_NOTIFY_EVT: { | ||||
|       if (this->processing_) | ||||
|         break; | ||||
|  | ||||
|       if (param->notify.conn_id != this->parent_->conn_id) { | ||||
|         ESP_LOGW(TAG, "[%s] Received notify event for unexpected parent conn: expect %x, got %x", | ||||
|                  this->get_name().c_str(), this->parent_->conn_id, param->notify.conn_id); | ||||
|         // FIXME: bug in BLEClient holding wrong conn_id. | ||||
|       } | ||||
|  | ||||
|       if (param->notify.handle != this->char_handle_status_) { | ||||
|         ESP_LOGW(TAG, "[%s] Unexpected notify handle, wanted %04X, got %04X", this->get_name().c_str(), | ||||
|                  this->char_handle_status_, param->notify.handle); | ||||
|         break; | ||||
|       } | ||||
|  | ||||
|       // FIXME: notify events come in every ~200-300 ms, which is too fast to be helpful. So we | ||||
|       //  throttle the updates to once every MIN_NOTIFY_THROTTLE (5 seconds). | ||||
|       //  Another idea would be to keep notify off by default, and use update() as an opportunity to turn on | ||||
|       //  notify to get enough data to update status, then turn off notify again. | ||||
|  | ||||
|       uint32_t now = millis(); | ||||
|       auto delta = now - this->last_notify_; | ||||
|  | ||||
|       if (!this->force_refresh_ && this->codec_->compare(param->notify.value, param->notify.value_len)) { | ||||
|         // If the packet is meaningfully different, trigger children as well | ||||
|         this->force_refresh_ = true; | ||||
|         ESP_LOGV(TAG, "[%s] Incoming packet indicates a significant change.", this->get_name().c_str()); | ||||
|       } | ||||
|  | ||||
|       if (this->last_notify_ == 0 || delta > MIN_NOTIFY_THROTTLE || this->force_refresh_) { | ||||
|         // Set reentrant flag to prevent processing multiple packets. | ||||
|         this->processing_ = true; | ||||
|         ESP_LOGVV(TAG, "[%s] Decoding packet: last=%d, delta=%d, force=%s", this->get_name().c_str(), | ||||
|                   this->last_notify_, delta, this->force_refresh_ ? "y" : "n"); | ||||
|         bool needs_extra = this->codec_->decode_notify(param->notify.value, param->notify.value_len); | ||||
|  | ||||
|         if (needs_extra) { | ||||
|           // This means the packet was partial, so read the status characteristic to get the second part. | ||||
|           // Ideally this will complete quickly. We won't process additional notification events until it does. | ||||
|           auto status = esp_ble_gattc_read_char(this->parent_->gattc_if, this->parent_->conn_id, | ||||
|                                                 this->char_handle_status_, ESP_GATT_AUTH_REQ_NONE); | ||||
|           if (status) { | ||||
|             ESP_LOGI(TAG, "[%s] Unable to read extended status packet", this->get_name().c_str()); | ||||
|           } | ||||
|         } else { | ||||
|           this->status_packet_ready_(); | ||||
|         } | ||||
|       } | ||||
|       break; | ||||
|     } | ||||
|     default: | ||||
|       ESP_LOGVV(TAG, "[%s] gattc unhandled event: enum=%d", this->get_name().c_str(), event); | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| inline void BedJetHub::status_packet_ready_() { | ||||
|   this->last_notify_ = millis(); | ||||
|   this->processing_ = false; | ||||
|  | ||||
|   if (this->force_refresh_) { | ||||
|     // If we requested an immediate update, do that now. | ||||
|     this->update(); | ||||
|     this->force_refresh_ = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| /** Reimplementation of BLEClient.gattc_event_handler() for ESP_GATTC_REG_FOR_NOTIFY_EVT. | ||||
|  * | ||||
|  * This is a copy of ble_client's automatic handling of `ESP_GATTC_REG_FOR_NOTIFY_EVT`, in order | ||||
|  * to undo the same on unregister. It also allows us to maintain the config descriptor separately, | ||||
|  * since the parent BLEClient is going to purge all descriptors once we set our connection status | ||||
|  * to `Established`. | ||||
|  */ | ||||
| uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { | ||||
|   auto handle = this->config_descr_status_; | ||||
|   if (handle == 0) { | ||||
|     ESP_LOGW(TAG, "No descriptor found for notify of handle 0x%x", this->char_handle_status_); | ||||
|     return -1; | ||||
|   } | ||||
|  | ||||
|   // NOTE: BLEClient uses `uint8_t*` of length 1, but BLE spec requires 16 bits. | ||||
|   uint16_t notify_en = enable ? 1 : 0; | ||||
|   auto status = | ||||
|       esp_ble_gattc_write_char_descr(this->parent_->gattc_if, this->parent_->conn_id, handle, sizeof(notify_en), | ||||
|                                      (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); | ||||
|     return status; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "[%s] wrote notify=%s to status config 0x%04x, for conn %d", this->get_name().c_str(), | ||||
|            enable ? "true" : "false", handle, this->parent_->conn_id); | ||||
|   return ESP_GATT_OK; | ||||
| } | ||||
|  | ||||
| /* Time Component */ | ||||
|  | ||||
| #ifdef USE_TIME | ||||
| void BedJetHub::send_local_time() { | ||||
|   if (this->time_id_.has_value()) { | ||||
|     auto *time_id = *this->time_id_; | ||||
|     time::ESPTime now = time_id->now(); | ||||
|     if (now.is_valid()) { | ||||
|       this->set_clock(now.hour, now.minute); | ||||
|       ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); | ||||
|     } | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetHub::setup_time_() { | ||||
|   if (this->time_id_.has_value()) { | ||||
|     this->send_local_time(); | ||||
|     auto *time_id = *this->time_id_; | ||||
|     time_id->add_on_time_sync_callback([this] { this->send_local_time(); }); | ||||
|   } else { | ||||
|     ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock."); | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { | ||||
|   if (!this->is_connected()) { | ||||
|     ESP_LOGV(TAG, "[%s] Not connected, cannot send time.", this->get_name().c_str()); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   BedjetPacket *pkt = this->codec_->get_set_time_request(hour, minute); | ||||
|   auto status = this->write_bedjet_packet_(pkt); | ||||
|   if (status) { | ||||
|     ESP_LOGW(TAG, "Failed setting BedJet clock: %d", status); | ||||
|   } else { | ||||
|     ESP_LOGD(TAG, "[%s] BedJet clock set to: %d:%02d", this->get_name().c_str(), hour, minute); | ||||
|   } | ||||
| } | ||||
|  | ||||
| /* Internal */ | ||||
|  | ||||
| void BedJetHub::loop() {} | ||||
| void BedJetHub::update() { this->dispatch_status_(); } | ||||
|  | ||||
| void BedJetHub::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "BedJet Hub '%s'", this->get_name().c_str()); | ||||
|   ESP_LOGCONFIG(TAG, "  ble_client.app_id: %d", this->parent()->app_id); | ||||
|   ESP_LOGCONFIG(TAG, "  ble_client.conn_id: %d", this->parent()->conn_id); | ||||
|   LOG_UPDATE_INTERVAL(this) | ||||
|   ESP_LOGCONFIG(TAG, "  Child components (%d):", this->children_.size()); | ||||
|   for (auto *child : this->children_) { | ||||
|     ESP_LOGCONFIG(TAG, "    - %s", child->describe().c_str()); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetHub::dispatch_state_(bool is_ready) { | ||||
|   for (auto *child : this->children_) { | ||||
|     child->on_bedjet_state(is_ready); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetHub::dispatch_status_() { | ||||
|   auto *status = this->codec_->get_status_packet(); | ||||
|  | ||||
|   if (!this->is_connected()) { | ||||
|     ESP_LOGD(TAG, "[%s] Not connected, will not send status.", this->get_name().c_str()); | ||||
|   } else if (status != nullptr) { | ||||
|     ESP_LOGD(TAG, "[%s] Notifying %d children of latest status @%p.", this->get_name().c_str(), this->children_.size(), | ||||
|              status); | ||||
|     for (auto *child : this->children_) { | ||||
|       child->on_status(status); | ||||
|     } | ||||
|   } else { | ||||
|     uint32_t now = millis(); | ||||
|     uint32_t diff = now - this->last_notify_; | ||||
|  | ||||
|     if (this->last_notify_ == 0) { | ||||
|       // This means we're connected and haven't received a notification, so it likely means that the BedJet is off. | ||||
|       // However, it could also mean that it's running, but failing to send notifications. | ||||
|       // We can try to unregister for notifications now, and then re-register, hoping to clear it up... | ||||
|       // But how do we know for sure which state we're in, and how do we actually clear out the buggy state? | ||||
|  | ||||
|       ESP_LOGI(TAG, "[%s] Still waiting for first GATT notify event.", this->get_name().c_str()); | ||||
|     } else if (diff > NOTIFY_WARN_THRESHOLD) { | ||||
|       ESP_LOGW(TAG, "[%s] Last GATT notify was %d seconds ago.", this->get_name().c_str(), diff / 1000); | ||||
|     } | ||||
|  | ||||
|     if (this->timeout_ > 0 && diff > this->timeout_ && this->parent()->enabled) { | ||||
|       ESP_LOGW(TAG, "[%s] Timed out after %d sec. Retrying...", this->get_name().c_str(), this->timeout_); | ||||
|       // set_enabled(false) will only close the connection if state != IDLE. | ||||
|       this->parent()->set_state(espbt::ClientState::CONNECTING); | ||||
|       this->parent()->set_enabled(false); | ||||
|       this->parent()->set_enabled(true); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BedJetHub::register_child(BedJetClient *obj) { | ||||
|   this->children_.push_back(obj); | ||||
|   obj->set_parent(this); | ||||
| } | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
							
								
								
									
										178
									
								
								esphome/components/bedjet/bedjet_hub.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								esphome/components/bedjet/bedjet_hub.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/defines.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "bedjet_child.h" | ||||
| #include "bedjet_codec.h" | ||||
|  | ||||
| #ifdef USE_TIME | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gattc_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace bedjet { | ||||
|  | ||||
| namespace espbt = esphome::esp32_ble_tracker; | ||||
|  | ||||
| // Forward declare BedJetClient | ||||
| class BedJetClient; | ||||
|  | ||||
| static const espbt::ESPBTUUID BEDJET_SERVICE_UUID = espbt::ESPBTUUID::from_raw("00001000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_STATUS_UUID = espbt::ESPBTUUID::from_raw("00002000-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_COMMAND_UUID = espbt::ESPBTUUID::from_raw("00002004-bed0-0080-aa55-4265644a6574"); | ||||
| static const espbt::ESPBTUUID BEDJET_NAME_UUID = espbt::ESPBTUUID::from_raw("00002001-bed0-0080-aa55-4265644a6574"); | ||||
|  | ||||
| /** | ||||
|  * Hub component connecting to the BedJet device over Bluetooth. | ||||
|  */ | ||||
| class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingComponent { | ||||
|  public: | ||||
|   /* BedJet functionality exposed to `BedJetClient` children and/or accessible from action lambdas. */ | ||||
|  | ||||
|   /** Attempts to check for and apply firmware updates. */ | ||||
|   void upgrade_firmware(); | ||||
|  | ||||
|   /** Press the OFF button. */ | ||||
|   bool button_off(); | ||||
|   /** Press the HEAT button. */ | ||||
|   bool button_heat(); | ||||
|   /** Press the EXT HT button. */ | ||||
|   bool button_ext_heat(); | ||||
|   /** Press the TURBO button. */ | ||||
|   bool button_turbo(); | ||||
|   /** Press the COOL button. */ | ||||
|   bool button_cool(); | ||||
|   /** Press the DRY button. */ | ||||
|   bool button_dry(); | ||||
|   /** Press the M1 (memory recall) button. */ | ||||
|   bool button_memory1(); | ||||
|   /** Press the M2 (memory recall) button. */ | ||||
|   bool button_memory2(); | ||||
|   /** Press the M3 (memory recall) button. */ | ||||
|   bool button_memory3(); | ||||
|  | ||||
|   /** Send the `button`. */ | ||||
|   bool send_button(BedjetButton button); | ||||
|  | ||||
|   /** Set the target temperature to `temp_c` in °C. */ | ||||
|   bool set_target_temp(float temp_c); | ||||
|  | ||||
|   /** Set the fan speed to a stepped index in the range 0-19. */ | ||||
|   bool set_fan_index(uint8_t fan_speed_index); | ||||
|  | ||||
|   /** Set the fan speed to a percent in the range 5% - 100%, at 5% increments. */ | ||||
|   bool set_fan_speed(uint8_t fan_speed_pct) { return this->set_fan_index(bedjet_fan_speed_to_index(fan_speed_pct)); } | ||||
|  | ||||
|   /** Return the fan speed index, in the range 0-19. */ | ||||
|   uint8_t get_fan_index(); | ||||
|  | ||||
|   /** Return the fan speed as a percent in the range 5%-100%. */ | ||||
|   uint8_t get_fan_speed() { return bedjet_fan_step_to_speed(this->get_fan_index()); } | ||||
|  | ||||
|   /** Set the operational runtime remaining. | ||||
|    * | ||||
|    * The unit establishes and enforces runtime limits for some modes, so this call is not guaranteed to succeed. | ||||
|    */ | ||||
|   bool set_time_remaining(uint8_t hours, uint8_t mins); | ||||
|  | ||||
|   /** Return the remaining runtime, in seconds. */ | ||||
|   uint16_t get_time_remaining(); | ||||
|  | ||||
|   /** @return `true` if the `BLEClient::node_state` is `ClientState::ESTABLISHED`. */ | ||||
|   bool is_connected() { return this->node_state == espbt::ClientState::ESTABLISHED; } | ||||
|  | ||||
|   bool has_status() { return this->codec_->has_status(); } | ||||
|   const BedjetStatusPacket *get_status_packet() const { return this->codec_->get_status_packet(); } | ||||
|  | ||||
|   /** Register a `BedJetClient` child component. */ | ||||
|   void register_child(BedJetClient *obj); | ||||
|  | ||||
|   /** Set the status timeout. | ||||
|    * | ||||
|    * This is the max time to wait for a status update before the connection is presumed unusable. | ||||
|    */ | ||||
|   void set_status_timeout(uint32_t timeout) { this->timeout_ = timeout; } | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   /** Set the `time::RealTimeClock` implementation. */ | ||||
|   void set_time_id(time::RealTimeClock *time_id) { this->time_id_ = time_id; } | ||||
|   /** Attempts to sync the local time (via `time_id`) to the BedJet device. */ | ||||
|   void send_local_time(); | ||||
| #endif | ||||
|   /** Attempt to set the BedJet device's clock to the specified time. */ | ||||
|   void set_clock(uint8_t hour, uint8_t minute); | ||||
|  | ||||
|   /* Component overrides */ | ||||
|  | ||||
|   void loop() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   void setup() override { this->codec_ = make_unique<BedjetCodec>(); } | ||||
|   float get_setup_priority() const override { return setup_priority::AFTER_WIFI; } | ||||
|  | ||||
|   /** @return The BedJet's configured name, or the MAC address if not discovered yet. */ | ||||
|   std::string get_name() { | ||||
|     if (this->name_.empty()) { | ||||
|       return this->parent_->address_str(); | ||||
|     } else { | ||||
|       return this->name_; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /* BLEClient overrides */ | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|  protected: | ||||
|   std::vector<BedJetClient *> children_; | ||||
|   void dispatch_status_(); | ||||
|   void dispatch_state_(bool is_ready); | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   /** Initializes time sync callbacks to support syncing current time to the BedJet. */ | ||||
|   void setup_time_(); | ||||
|   optional<time::RealTimeClock *> time_id_{}; | ||||
| #endif | ||||
|  | ||||
|   uint32_t timeout_{DEFAULT_STATUS_TIMEOUT}; | ||||
|   static const uint32_t MIN_NOTIFY_THROTTLE = 15000; | ||||
|   static const uint32_t NOTIFY_WARN_THRESHOLD = 300000; | ||||
|   static const uint32_t DEFAULT_STATUS_TIMEOUT = 900000; | ||||
|  | ||||
|   uint8_t set_notify_(bool enable); | ||||
|   /** Send the `BedjetPacket` to the device. */ | ||||
|   uint8_t write_bedjet_packet_(BedjetPacket *pkt); | ||||
|   void set_name_(const std::string &name) { this->name_ = name; } | ||||
|  | ||||
|   std::string name_; | ||||
|  | ||||
|   uint32_t last_notify_ = 0; | ||||
|   inline void status_packet_ready_(); | ||||
|   bool force_refresh_ = false; | ||||
|   bool processing_ = false; | ||||
|  | ||||
|   std::unique_ptr<BedjetCodec> codec_; | ||||
|  | ||||
|   bool discover_characteristics_(); | ||||
|   uint16_t char_handle_cmd_; | ||||
|   uint16_t char_handle_name_; | ||||
|   uint16_t char_handle_status_; | ||||
|   uint16_t config_descr_status_; | ||||
|  | ||||
|   uint8_t open_conn_id_ = -1; | ||||
|  | ||||
|   uint8_t write_notify_config_descriptor_(bool enable); | ||||
| }; | ||||
|  | ||||
| }  // namespace bedjet | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,32 +1,60 @@ | ||||
| import logging | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import climate, ble_client, time | ||||
| from esphome.components import climate, ble_client | ||||
| from esphome.const import ( | ||||
|     CONF_HEAT_MODE, | ||||
|     CONF_ID, | ||||
|     CONF_RECEIVE_TIMEOUT, | ||||
|     CONF_TIME_ID, | ||||
| ) | ||||
| from . import ( | ||||
|     BEDJET_CLIENT_SCHEMA, | ||||
|     register_bedjet_child, | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
| CODEOWNERS = ["@jhansche"] | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| bedjet_ns = cg.esphome_ns.namespace("bedjet") | ||||
| Bedjet = bedjet_ns.class_( | ||||
|     "Bedjet", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||
| BedJetClimate = bedjet_ns.class_( | ||||
|     "BedJetClimate", climate.Climate, ble_client.BLEClientNode, cg.PollingComponent | ||||
| ) | ||||
| BedjetHeatMode = bedjet_ns.enum("BedjetHeatMode") | ||||
| BEDJET_HEAT_MODES = { | ||||
|     "heat": BedjetHeatMode.HEAT_MODE_HEAT, | ||||
|     "extended": BedjetHeatMode.HEAT_MODE_EXTENDED, | ||||
| } | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     climate.CLIMATE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(Bedjet), | ||||
|             cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock), | ||||
|             cv.Optional( | ||||
|                 CONF_RECEIVE_TIMEOUT, default="0s" | ||||
|             ): cv.positive_time_period_milliseconds, | ||||
|             cv.GenerateID(): cv.declare_id(BedJetClimate), | ||||
|             cv.Optional(CONF_HEAT_MODE, default="heat"): cv.enum( | ||||
|                 BEDJET_HEAT_MODES, lower=True | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("30s")) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend( | ||||
|         # TODO: remove compat layer. | ||||
|         { | ||||
|             cv.Optional(ble_client.CONF_BLE_CLIENT_ID): cv.invalid( | ||||
|                 "The 'ble_client_id' option has been removed. Please migrate " | ||||
|                 "to the new `bedjet_id` option in the `bedjet` component.\n" | ||||
|                 "See https://esphome.io/components/climate/bedjet.html" | ||||
|             ), | ||||
|             cv.Optional(CONF_TIME_ID): cv.invalid( | ||||
|                 "The 'time_id' option has been moved to the `bedjet` component." | ||||
|             ), | ||||
|             cv.Optional(CONF_RECEIVE_TIMEOUT): cv.invalid( | ||||
|                 "The 'receive_timeout' option has been moved to the `bedjet` component." | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(BEDJET_CLIENT_SCHEMA) | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -34,9 +62,6 @@ async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await climate.register_climate(var, config) | ||||
|     await ble_client.register_ble_node(var, config) | ||||
|     if CONF_TIME_ID in config: | ||||
|         time_ = await cg.get_variable(config[CONF_TIME_ID]) | ||||
|         cg.add(var.set_time_id(time_)) | ||||
|     if CONF_RECEIVE_TIMEOUT in config: | ||||
|         cg.add(var.set_status_timeout(config[CONF_RECEIVE_TIMEOUT])) | ||||
|     await register_bedjet_child(var, config) | ||||
|  | ||||
|     cg.add(var.set_heating_mode(config[CONF_HEAT_MODE])) | ||||
|   | ||||
| @@ -22,6 +22,7 @@ from esphome.const import ( | ||||
|     CONF_ON_PRESS, | ||||
|     CONF_ON_RELEASE, | ||||
|     CONF_ON_STATE, | ||||
|     CONF_PUBLISH_INITIAL_STATE, | ||||
|     CONF_STATE, | ||||
|     CONF_TIMING, | ||||
|     CONF_TRIGGER_ID, | ||||
| @@ -29,6 +30,7 @@ from esphome.const import ( | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_BATTERY_CHARGING, | ||||
|     DEVICE_CLASS_CARBON_MONOXIDE, | ||||
|     DEVICE_CLASS_COLD, | ||||
|     DEVICE_CLASS_CONNECTIVITY, | ||||
|     DEVICE_CLASS_DOOR, | ||||
| @@ -63,6 +65,7 @@ DEVICE_CLASSES = [ | ||||
|     DEVICE_CLASS_EMPTY, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_BATTERY_CHARGING, | ||||
|     DEVICE_CLASS_CARBON_MONOXIDE, | ||||
|     DEVICE_CLASS_COLD, | ||||
|     DEVICE_CLASS_CONNECTIVITY, | ||||
|     DEVICE_CLASS_DOOR, | ||||
| @@ -326,6 +329,7 @@ BINARY_SENSOR_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(cv.MQTT_COMPONENT_SCHEMA).ex | ||||
|         cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id( | ||||
|             mqtt.MQTTBinarySensorComponent | ||||
|         ), | ||||
|         cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean, | ||||
|         cv.Optional(CONF_DEVICE_CLASS): validate_device_class, | ||||
|         cv.Optional(CONF_FILTERS): validate_filters, | ||||
|         cv.Optional(CONF_ON_PRESS): automation.validate_automation( | ||||
| @@ -418,6 +422,8 @@ async def setup_binary_sensor_core_(var, config): | ||||
|  | ||||
|     if CONF_DEVICE_CLASS in config: | ||||
|         cg.add(var.set_device_class(config[CONF_DEVICE_CLASS])) | ||||
|     if CONF_PUBLISH_INITIAL_STATE in config: | ||||
|         cg.add(var.set_publish_initial_state(config[CONF_PUBLISH_INITIAL_STATE])) | ||||
|     if CONF_INVERTED in config: | ||||
|         cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     if CONF_FILTERS in config: | ||||
| @@ -477,8 +483,8 @@ async def register_binary_sensor(var, config): | ||||
|     await setup_binary_sensor_core_(var, config) | ||||
|  | ||||
|  | ||||
| async def new_binary_sensor(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
| async def new_binary_sensor(config, *args): | ||||
|     var = cg.new_Pvariable(config[CONF_ID], *args) | ||||
|     await register_binary_sensor(var, config) | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -37,7 +37,7 @@ void BinarySensor::send_state_internal(bool state, bool is_initial) { | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->state = state; | ||||
|   if (!is_initial) { | ||||
|   if (!is_initial || this->publish_initial_state_) { | ||||
|     this->state_callback_.call(state); | ||||
|   } | ||||
| } | ||||
| @@ -69,7 +69,6 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) { | ||||
|   } | ||||
| } | ||||
| bool BinarySensor::has_state() const { return this->has_state_; } | ||||
| uint32_t BinarySensor::hash_base() { return 1210250844UL; } | ||||
| bool BinarySensor::is_status_binary_sensor() const { return false; } | ||||
|  | ||||
| }  // namespace binary_sensor | ||||
|   | ||||
| @@ -58,6 +58,8 @@ class BinarySensor : public EntityBase { | ||||
|   void add_filter(Filter *filter); | ||||
|   void add_filters(const std::vector<Filter *> &filters); | ||||
|  | ||||
|   void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; } | ||||
|  | ||||
|   // ========== INTERNAL METHODS ========== | ||||
|   // (In most use cases you won't need these) | ||||
|   void send_state_internal(bool state, bool is_initial); | ||||
| @@ -76,12 +78,11 @@ class BinarySensor : public EntityBase { | ||||
|   virtual std::string device_class(); | ||||
|  | ||||
|  protected: | ||||
|   uint32_t hash_base() override; | ||||
|  | ||||
|   CallbackManager<void(bool)> state_callback_{}; | ||||
|   optional<std::string> device_class_{};  ///< Stores the override of the device class | ||||
|   Filter *filter_list_{nullptr}; | ||||
|   bool has_state_{false}; | ||||
|   bool publish_initial_state_{false}; | ||||
|   Deduplicator<bool> publish_dedup_; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,7 @@ namespace bl0939 { | ||||
| static const char *const TAG = "bl0939"; | ||||
|  | ||||
| // https://www.belling.com.cn/media/file_object/bel_product/BL0939/datasheet/BL0939_V1.2_cn.pdf | ||||
| // (unfortunatelly chinese, but the protocol can be understood with some translation tool) | ||||
| // (unfortunately chinese, but the protocol can be understood with some translation tool) | ||||
| static const uint8_t BL0939_READ_COMMAND = 0x55;  // 0x5{A4,A3,A2,A1} | ||||
| static const uint8_t BL0939_FULL_PACKET = 0xAA; | ||||
| static const uint8_t BL0939_PACKET_HEADER = 0x55; | ||||
|   | ||||
| @@ -8,7 +8,7 @@ namespace esphome { | ||||
| namespace bl0939 { | ||||
|  | ||||
| // https://datasheet.lcsc.com/lcsc/2108071830_BL-Shanghai-Belling-BL0939_C2841044.pdf | ||||
| // (unfortunatelly chinese, but the formulas can be easily understood) | ||||
| // (unfortunately chinese, but the formulas can be easily understood) | ||||
| // Sonoff Dual R3 V2 has the exact same resistor values for the current shunts (RL=1miliOhm) | ||||
| // and for the voltage divider (R1=0.51kOhm, R2=5*390kOhm) | ||||
| // as in the manufacturer's reference circuit, so the same formulas were used here (Vref=1.218V) | ||||
|   | ||||
| @@ -2,12 +2,15 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import esp32_ble_tracker | ||||
| from esphome.const import ( | ||||
|     CONF_CHARACTERISTIC_UUID, | ||||
|     CONF_ID, | ||||
|     CONF_MAC_ADDRESS, | ||||
|     CONF_NAME, | ||||
|     CONF_ON_CONNECT, | ||||
|     CONF_ON_DISCONNECT, | ||||
|     CONF_SERVICE_UUID, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_VALUE, | ||||
| ) | ||||
| from esphome import automation | ||||
|  | ||||
| @@ -27,6 +30,8 @@ BLEClientConnectTrigger = ble_client_ns.class_( | ||||
| BLEClientDisconnectTrigger = ble_client_ns.class_( | ||||
|     "BLEClientDisconnectTrigger", automation.Trigger.template(BLEClientNodeConstRef) | ||||
| ) | ||||
| # Actions | ||||
| BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action) | ||||
|  | ||||
| # Espressif platformio framework is built with MAX_BLE_CONN to 3, so | ||||
| # enforce this in yaml checks. | ||||
| @@ -72,6 +77,33 @@ async def register_ble_node(var, config): | ||||
|     cg.add(parent.register_ble_node(var)) | ||||
|  | ||||
|  | ||||
| BLE_WRITE_ACTION_SCHEMA = cv.Schema( | ||||
|     { | ||||
|         cv.Required(CONF_ID): cv.use_id(BLEClient), | ||||
|         cv.Required(CONF_SERVICE_UUID): esp32_ble_tracker.bt_uuid, | ||||
|         cv.Required(CONF_CHARACTERISTIC_UUID): esp32_ble_tracker.bt_uuid, | ||||
|         cv.Required(CONF_VALUE): cv.ensure_list(cv.hex_uint8_t), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|     "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA | ||||
| ) | ||||
| async def ble_write_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) | ||||
|     value = config[CONF_VALUE] | ||||
|     cg.add(var.set_value(value)) | ||||
|     serv_uuid128 = esp32_ble_tracker.as_reversed_hex_array(config[CONF_SERVICE_UUID]) | ||||
|     cg.add(var.set_service_uuid128(serv_uuid128)) | ||||
|     char_uuid128 = esp32_ble_tracker.as_reversed_hex_array( | ||||
|         config[CONF_CHARACTERISTIC_UUID] | ||||
|     ) | ||||
|     cg.add(var.set_char_uuid128(char_uuid128)) | ||||
|     return var | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
							
								
								
									
										74
									
								
								esphome/components/ble_client/automation.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								esphome/components/ble_client/automation.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| #include "automation.h" | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gattc_api.h> | ||||
| #include <esp_bt_defs.h> | ||||
|  | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace ble_client { | ||||
| static const char *const TAG = "ble_client.automation"; | ||||
|  | ||||
| void BLEWriterClientNode::write() { | ||||
|   if (this->node_state != espbt::ClientState::ESTABLISHED) { | ||||
|     ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected"); | ||||
|     return; | ||||
|   } else if (this->ble_char_handle_ == 0) { | ||||
|     ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found"); | ||||
|     return; | ||||
|   } | ||||
|   esp_gatt_write_type_t write_type; | ||||
|   if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) { | ||||
|     write_type = ESP_GATT_WRITE_TYPE_RSP; | ||||
|     ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP"); | ||||
|   } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) { | ||||
|     write_type = ESP_GATT_WRITE_TYPE_NO_RSP; | ||||
|     ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP"); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   ESP_LOGVV(TAG, "Will write %d bytes: %s", this->value_.size(), format_hex_pretty(this->value_).c_str()); | ||||
|   esp_err_t err = esp_ble_gattc_write_char(this->parent()->gattc_if, this->parent()->conn_id, this->ble_char_handle_, | ||||
|                                            value_.size(), value_.data(), write_type, ESP_GATT_AUTH_REQ_NONE); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err)); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                               esp_ble_gattc_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GATTC_REG_EVT: | ||||
|       break; | ||||
|     case ESP_GATTC_OPEN_EVT: | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|       ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str()); | ||||
|       break; | ||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||
|       auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_); | ||||
|       if (chr == nullptr) { | ||||
|         ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s", | ||||
|                  this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       this->ble_char_handle_ = chr->handle; | ||||
|       this->char_props_ = chr->properties; | ||||
|       this->node_state = espbt::ClientState::ESTABLISHED; | ||||
|       ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(), | ||||
|                ble_client_->address_str().c_str()); | ||||
|       break; | ||||
|     } | ||||
|     case ESP_GATTC_DISCONNECT_EVT: | ||||
|       this->node_state = espbt::ClientState::IDLE; | ||||
|       this->ble_char_handle_ = 0; | ||||
|       ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str()); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
|  | ||||
| }  // namespace ble_client | ||||
| }  // namespace esphome | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/components/ble_client/ble_client.h" | ||||
|  | ||||
| @@ -33,6 +35,41 @@ class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode { | ||||
|   } | ||||
| }; | ||||
|  | ||||
| class BLEWriterClientNode : public BLEClientNode { | ||||
|  public: | ||||
|   BLEWriterClientNode(BLEClient *ble_client) { | ||||
|     ble_client->register_ble_node(this); | ||||
|     ble_client_ = ble_client; | ||||
|   } | ||||
|  | ||||
|   void set_value(std::vector<uint8_t> value) { value_ = std::move(value); } | ||||
|  | ||||
|   // Attempts to write the contents of value_ to char_uuid_. | ||||
|   void write(); | ||||
|  | ||||
|   void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|  | ||||
|   void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); } | ||||
|  | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|  | ||||
|  private: | ||||
|   BLEClient *ble_client_; | ||||
|   int ble_char_handle_ = 0; | ||||
|   esp_gatt_char_prop_t char_props_; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   std::vector<uint8_t> value_; | ||||
| }; | ||||
|  | ||||
| template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEWriterClientNode { | ||||
|  public: | ||||
|   BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {} | ||||
|  | ||||
|   void play(Ts... x) override { return write(); } | ||||
| }; | ||||
|  | ||||
| }  // namespace ble_client | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -113,6 +113,7 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es | ||||
|     } | ||||
|     case ESP_GATTC_OPEN_EVT: { | ||||
|       ESP_LOGV(TAG, "[%s] ESP_GATTC_OPEN_EVT", this->address_str().c_str()); | ||||
|       this->conn_id = param->open.conn_id; | ||||
|       if (param->open.status != ESP_GATT_OK) { | ||||
|         ESP_LOGW(TAG, "connect to %s failed, status=%d", this->address_str().c_str(), param->open.status); | ||||
|         this->set_states_(espbt::ClientState::IDLE); | ||||
| @@ -122,7 +123,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es | ||||
|     } | ||||
|     case ESP_GATTC_CONNECT_EVT: { | ||||
|       ESP_LOGV(TAG, "[%s] ESP_GATTC_CONNECT_EVT", this->address_str().c_str()); | ||||
|       this->conn_id = param->connect.conn_id; | ||||
|       if (this->conn_id != param->connect.conn_id) { | ||||
|         ESP_LOGD(TAG, "[%s] Unexpected conn_id in CONNECT_EVT: param conn=%d, open conn=%d", | ||||
|                  this->address_str().c_str(), param->connect.conn_id, this->conn_id); | ||||
|       } | ||||
|       auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if, param->connect.conn_id); | ||||
|       if (ret) { | ||||
|         ESP_LOGW(TAG, "esp_ble_gattc_send_mtu_req failed, status=%x", ret); | ||||
| @@ -183,9 +187,10 @@ void BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t es | ||||
|                  descr->uuid.to_string().c_str()); | ||||
|         break; | ||||
|       } | ||||
|       uint8_t notify_en = 1; | ||||
|       auto status = esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), | ||||
|                                                    ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|       uint16_t notify_en = 1; | ||||
|       auto status = | ||||
|           esp_ble_gattc_write_char_descr(this->gattc_if, this->conn_id, descr->handle, sizeof(notify_en), | ||||
|                                          (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE); | ||||
|       if (status) { | ||||
|         ESP_LOGW(TAG, "esp_ble_gattc_write_char_descr error, status=%d", status); | ||||
|       } | ||||
|   | ||||
| @@ -1,13 +1,12 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import ble_client, esp32_ble_tracker, output | ||||
| from esphome.const import CONF_ID, CONF_SERVICE_UUID | ||||
| from esphome.const import CONF_CHARACTERISTIC_UUID, CONF_ID, CONF_SERVICE_UUID | ||||
|  | ||||
| from .. import ble_client_ns | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| CONF_CHARACTERISTIC_UUID = "characteristic_uuid" | ||||
| CONF_REQUIRE_RESPONSE = "require_response" | ||||
|  | ||||
| BLEBinaryOutput = ble_client_ns.class_( | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import sensor, ble_client, esp32_ble_tracker | ||||
| from esphome.const import ( | ||||
|     CONF_CHARACTERISTIC_UUID, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_SERVICE_UUID, | ||||
| @@ -11,7 +12,6 @@ from .. import ble_client_ns | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| CONF_CHARACTERISTIC_UUID = "characteristic_uuid" | ||||
| CONF_DESCRIPTOR_UUID = "descriptor_uuid" | ||||
|  | ||||
| CONF_NOTIFY = "notify" | ||||
|   | ||||
| @@ -11,8 +11,6 @@ namespace ble_client { | ||||
|  | ||||
| static const char *const TAG = "ble_sensor"; | ||||
|  | ||||
| uint32_t BLESensor::hash_base() { return 343459825UL; } | ||||
|  | ||||
| void BLESensor::loop() {} | ||||
|  | ||||
| void BLESensor::dump_config() { | ||||
|   | ||||
| @@ -37,7 +37,6 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie | ||||
|   uint16_t handle; | ||||
|  | ||||
|  protected: | ||||
|   uint32_t hash_base() override; | ||||
|   float parse_data_(uint8_t *value, uint16_t value_len); | ||||
|   optional<data_to_value_t> data_to_value_func_{}; | ||||
|   bool notify_; | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import text_sensor, ble_client, esp32_ble_tracker | ||||
| from esphome.const import ( | ||||
|     CONF_CHARACTERISTIC_UUID, | ||||
|     CONF_ID, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_SERVICE_UUID, | ||||
| @@ -11,7 +12,6 @@ from .. import ble_client_ns | ||||
|  | ||||
| DEPENDENCIES = ["ble_client"] | ||||
|  | ||||
| CONF_CHARACTERISTIC_UUID = "characteristic_uuid" | ||||
| CONF_DESCRIPTOR_UUID = "descriptor_uuid" | ||||
|  | ||||
| CONF_NOTIFY = "notify" | ||||
|   | ||||
| @@ -14,8 +14,6 @@ static const char *const TAG = "ble_text_sensor"; | ||||
|  | ||||
| static const std::string EMPTY = ""; | ||||
|  | ||||
| uint32_t BLETextSensor::hash_base() { return 193967603UL; } | ||||
|  | ||||
| void BLETextSensor::loop() {} | ||||
|  | ||||
| void BLETextSensor::dump_config() { | ||||
|   | ||||
| @@ -35,7 +35,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p | ||||
|   uint16_t handle; | ||||
|  | ||||
|  protected: | ||||
|   uint32_t hash_base() override; | ||||
|   bool notify_; | ||||
|   espbt::ESPBTUUID service_uuid_; | ||||
|   espbt::ESPBTUUID char_uuid_; | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| #include "bme280.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -28,6 +29,7 @@ static const uint8_t BME280_REGISTER_DIG_H5 = 0xE5; | ||||
| static const uint8_t BME280_REGISTER_DIG_H6 = 0xE7; | ||||
|  | ||||
| static const uint8_t BME280_REGISTER_CHIPID = 0xD0; | ||||
| static const uint8_t BME280_REGISTER_RESET = 0xE0; | ||||
|  | ||||
| static const uint8_t BME280_REGISTER_CONTROLHUMID = 0xF2; | ||||
| static const uint8_t BME280_REGISTER_STATUS = 0xF3; | ||||
| @@ -39,6 +41,8 @@ static const uint8_t BME280_REGISTER_TEMPDATA = 0xFA; | ||||
| static const uint8_t BME280_REGISTER_HUMIDDATA = 0xFD; | ||||
|  | ||||
| static const uint8_t BME280_MODE_FORCED = 0b01; | ||||
| static const uint8_t BME280_SOFT_RESET = 0xB6; | ||||
| static const uint8_t BME280_STATUS_IM_UPDATE = 0b01; | ||||
|  | ||||
| inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); } | ||||
|  | ||||
| @@ -97,6 +101,28 @@ void BME280Component::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Send a soft reset. | ||||
|   if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   // Wait until the NVM data has finished loading. | ||||
|   uint8_t status; | ||||
|   uint8_t retry = 5; | ||||
|   do { | ||||
|     delay(2); | ||||
|     if (!this->read_byte(BME280_REGISTER_STATUS, &status)) { | ||||
|       ESP_LOGW(TAG, "Error reading status register."); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
|   } while ((status & BME280_STATUS_IM_UPDATE) && (--retry)); | ||||
|   if (status & BME280_STATUS_IM_UPDATE) { | ||||
|     ESP_LOGW(TAG, "Timeout loading NVM."); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Read calibration | ||||
|   this->calibration_.t1 = read_u16_le_(BME280_REGISTER_DIG_T1); | ||||
|   this->calibration_.t2 = read_s16_le_(BME280_REGISTER_DIG_T2); | ||||
|   | ||||
| @@ -25,7 +25,7 @@ OVERSAMPLING_OPTIONS = { | ||||
|     "4X": Oversampling.OVERSAMPLING_X4, | ||||
|     "8X": Oversampling.OVERSAMPLING_X8, | ||||
|     "16X": Oversampling.OVERSAMPLING_X16, | ||||
|     "32x": Oversampling.OVERSAMPLING_X32, | ||||
|     "32X": Oversampling.OVERSAMPLING_X32, | ||||
| } | ||||
|  | ||||
| IIRFilter = bmp3xx_ns.enum("IIRFilter") | ||||
|   | ||||
| @@ -102,8 +102,8 @@ async def register_button(var, config): | ||||
|     await setup_button_core_(var, config) | ||||
|  | ||||
|  | ||||
| async def new_button(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
| async def new_button(config, *args): | ||||
|     var = cg.new_Pvariable(config[CONF_ID], *args) | ||||
|     await register_button(var, config) | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -15,7 +15,6 @@ void Button::press() { | ||||
|   this->press_callback_.call(); | ||||
| } | ||||
| void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); } | ||||
| uint32_t Button::hash_base() { return 1495763804UL; } | ||||
|  | ||||
| void Button::set_device_class(const std::string &device_class) { this->device_class_ = device_class; } | ||||
| std::string Button::get_device_class() { return this->device_class_; } | ||||
|   | ||||
| @@ -47,8 +47,6 @@ class Button : public EntityBase { | ||||
|    */ | ||||
|   virtual void press_action() = 0; | ||||
|  | ||||
|   uint32_t hash_base() override; | ||||
|  | ||||
|   CallbackManager<void()> press_callback_{}; | ||||
|   std::string device_class_{}; | ||||
| }; | ||||
|   | ||||
| @@ -419,7 +419,6 @@ void Climate::publish_state() { | ||||
|   // Save state | ||||
|   this->save_state_(); | ||||
| } | ||||
| uint32_t Climate::hash_base() { return 3104134496UL; } | ||||
|  | ||||
| ClimateTraits Climate::get_traits() { | ||||
|   auto traits = this->traits(); | ||||
|   | ||||
| @@ -282,7 +282,6 @@ class Climate : public EntityBase { | ||||
|    */ | ||||
|   void save_state_(); | ||||
|  | ||||
|   uint32_t hash_base() override; | ||||
|   void dump_traits_(const char *tag); | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
|   | ||||
| @@ -33,8 +33,6 @@ const char *cover_operation_to_str(CoverOperation op) { | ||||
|  | ||||
| Cover::Cover(const std::string &name) : EntityBase(name), position{COVER_OPEN} {} | ||||
|  | ||||
| uint32_t Cover::hash_base() { return 1727367479UL; } | ||||
|  | ||||
| CoverCall::CoverCall(Cover *parent) : parent_(parent) {} | ||||
| CoverCall &CoverCall::set_command(const char *command) { | ||||
|   if (strcasecmp(command, "OPEN") == 0) { | ||||
|   | ||||
| @@ -177,7 +177,6 @@ class Cover : public EntityBase { | ||||
|   virtual std::string device_class(); | ||||
|  | ||||
|   optional<CoverRestoreState> restore_state_(); | ||||
|   uint32_t hash_base() override; | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
|   optional<std::string> device_class_override_{}; | ||||
|   | ||||
| @@ -131,7 +131,7 @@ void CurrentBasedCover::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "  Close Duration: %.1fs", this->close_duration_ / 1e3f); | ||||
|   ESP_LOGCONFIG(TAG, "Obstacle Rollback: %.1f%%", this->obstacle_rollback_ * 100); | ||||
|   if (this->max_duration_ != UINT32_MAX) { | ||||
|     ESP_LOGCONFIG(TAG, "Maximun duration: %.1fs", this->max_duration_ / 1e3f); | ||||
|     ESP_LOGCONFIG(TAG, "Maximum duration: %.1fs", this->max_duration_ / 1e3f); | ||||
|   } | ||||
|   ESP_LOGCONFIG(TAG, "Start sensing delay: %.1fs", this->start_sensing_delay_ / 1e3f); | ||||
|   ESP_LOGCONFIG(TAG, "Malfunction detection: %s", YESNO(this->malfunction_detection_)); | ||||
|   | ||||
							
								
								
									
										32
									
								
								esphome/components/dac7678/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/dac7678/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import i2c | ||||
| from esphome.const import CONF_ID | ||||
|  | ||||
| AUTO_LOAD = ["output"] | ||||
| CODEOWNERS = ["@NickB1"] | ||||
| DEPENDENCIES = ["i2c"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| dac7678_ns = cg.esphome_ns.namespace("dac7678") | ||||
| DAC7678Output = dac7678_ns.class_("DAC7678Output", cg.Component, i2c.I2CDevice) | ||||
| CONF_INTERNAL_REFERENCE = "internal_reference" | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(DAC7678Output), | ||||
|             cv.Optional(CONF_INTERNAL_REFERENCE, default=False): cv.boolean, | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.COMPONENT_SCHEMA) | ||||
|     .extend(i2c.i2c_device_schema(0x48)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     cg.add(var.set_internal_reference(config[CONF_INTERNAL_REFERENCE])) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|     return var | ||||
							
								
								
									
										83
									
								
								esphome/components/dac7678/dac7678_output.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								esphome/components/dac7678/dac7678_output.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | ||||
| #include "dac7678_output.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/hal.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace dac7678 { | ||||
|  | ||||
| static const char *const TAG = "dac7678"; | ||||
|  | ||||
| static const uint8_t DAC7678_REG_INPUT_N = 0x00; | ||||
| static const uint8_t DAC7678_REG_SELECT_UPDATE_N = 0x10; | ||||
| static const uint8_t DAC7678_REG_WRITE_N_UPDATE_ALL = 0x20; | ||||
| static const uint8_t DAC7678_REG_WRITE_N_UPDATE_N = 0x30; | ||||
| static const uint8_t DAC7678_REG_POWER = 0x40; | ||||
| static const uint8_t DAC7678_REG_CLEAR_CODE = 0x50; | ||||
| static const uint8_t DAC7678_REG_LDAC = 0x60; | ||||
| static const uint8_t DAC7678_REG_SOFTWARE_RESET = 0x70; | ||||
| static const uint8_t DAC7678_REG_INTERNAL_REF_0 = 0x80; | ||||
| static const uint8_t DAC7678_REG_INTERNAL_REF_1 = 0x90; | ||||
|  | ||||
| void DAC7678Output::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Setting up DAC7678OutputComponent..."); | ||||
|  | ||||
|   ESP_LOGV(TAG, "Resetting device..."); | ||||
|  | ||||
|   // Reset device | ||||
|   if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) { | ||||
|     ESP_LOGE(TAG, "Reset failed"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } else | ||||
|     ESP_LOGV(TAG, "Reset succeeded"); | ||||
|  | ||||
|   delayMicroseconds(1000); | ||||
|  | ||||
|   // Set internal reference | ||||
|   if (this->internal_reference_) { | ||||
|     if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) { | ||||
|       ESP_LOGE(TAG, "Set internal reference failed"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } else | ||||
|       ESP_LOGV(TAG, "Internal reference enabled"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void DAC7678Output::dump_config() { | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Setting up DAC7678 failed!"); | ||||
|   } else | ||||
|     ESP_LOGCONFIG(TAG, "DAC7678 initialised"); | ||||
| } | ||||
|  | ||||
| void DAC7678Output::register_channel(DAC7678Channel *channel) { | ||||
|   auto c = channel->channel_; | ||||
|   this->min_channel_ = std::min(this->min_channel_, c); | ||||
|   this->max_channel_ = std::max(this->max_channel_, c); | ||||
|   channel->set_parent(this); | ||||
|   ESP_LOGV(TAG, "Registered channel: %01u", channel->channel_); | ||||
| } | ||||
|  | ||||
| void DAC7678Output::set_channel_value_(uint8_t channel, uint16_t value) { | ||||
|   if (this->dac_input_reg_[channel] != value) { | ||||
|     ESP_LOGV(TAG, "Channel %01u: input_reg=%04u ", channel, value); | ||||
|  | ||||
|     if (!this->write_byte_16(DAC7678_REG_WRITE_N_UPDATE_N | channel, value << 4)) { | ||||
|       this->status_set_warning(); | ||||
|       return; | ||||
|     } | ||||
|   } | ||||
|   this->dac_input_reg_[channel] = value; | ||||
|   this->status_clear_warning(); | ||||
| } | ||||
|  | ||||
| void DAC7678Channel::write_state(float state) { | ||||
|   const float input_rounded = roundf(state * this->full_scale_); | ||||
|   auto input = static_cast<uint16_t>(input_rounded); | ||||
|   this->parent_->set_channel_value_(this->channel_, input); | ||||
| } | ||||
|  | ||||
| }  // namespace dac7678 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										55
									
								
								esphome/components/dac7678/dac7678_output.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								esphome/components/dac7678/dac7678_output.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/output/float_output.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace dac7678 { | ||||
|  | ||||
| class DAC7678Output; | ||||
|  | ||||
| class DAC7678Channel : public output::FloatOutput, public Parented<DAC7678Output> { | ||||
|  public: | ||||
|   void set_channel(uint8_t channel) { channel_ = channel; } | ||||
|  | ||||
|  protected: | ||||
|   friend class DAC7678Output; | ||||
|  | ||||
|   const uint16_t full_scale_ = 0xFFF; | ||||
|  | ||||
|   void write_state(float state) override; | ||||
|  | ||||
|   uint8_t channel_; | ||||
| }; | ||||
|  | ||||
| /// DAC7678 float output component. | ||||
| class DAC7678Output : public Component, public i2c::I2CDevice { | ||||
|  public: | ||||
|   DAC7678Output() {} | ||||
|  | ||||
|   void register_channel(DAC7678Channel *channel); | ||||
|  | ||||
|   void set_internal_reference(const bool value) { this->internal_reference_ = value; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   friend DAC7678Channel; | ||||
|  | ||||
|   bool internal_reference_; | ||||
|  | ||||
|   void set_channel_value_(uint8_t channel, uint16_t value); | ||||
|  | ||||
|   uint8_t min_channel_{0xFF}; | ||||
|   uint8_t max_channel_{0x00}; | ||||
|   uint16_t dac_input_reg_[8] = { | ||||
|       0, | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| }  // namespace dac7678 | ||||
| }  // namespace esphome | ||||
							
								
								
									
										27
									
								
								esphome/components/dac7678/output.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								esphome/components/dac7678/output.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import output | ||||
| from esphome.const import CONF_CHANNEL, CONF_ID | ||||
| from . import DAC7678Output, dac7678_ns | ||||
|  | ||||
| DEPENDENCIES = ["dac7678"] | ||||
|  | ||||
| DAC7678Channel = dac7678_ns.class_("DAC7678Channel", output.FloatOutput) | ||||
| CONF_DAC7678_ID = "dac7678_id" | ||||
|  | ||||
| CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend( | ||||
|     { | ||||
|         cv.Required(CONF_ID): cv.declare_id(DAC7678Channel), | ||||
|         cv.GenerateID(CONF_DAC7678_ID): cv.use_id(DAC7678Output), | ||||
|         cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=7), | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     paren = await cg.get_variable(config[CONF_DAC7678_ID]) | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(var.set_channel(config[CONF_CHANNEL])) | ||||
|     cg.add(paren.register_channel(var)) | ||||
|     await output.register_output(var, config) | ||||
|     return var | ||||
| @@ -134,7 +134,6 @@ void DallasComponent::update() { | ||||
|         return; | ||||
|       } | ||||
|       if (!sensor->check_scratch_pad()) { | ||||
|         ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", sensor->get_name().c_str()); | ||||
|         sensor->publish_state(NAN); | ||||
|         this->status_set_warning(); | ||||
|         return; | ||||
| @@ -241,13 +240,29 @@ bool DallasTemperatureSensor::setup_sensor() { | ||||
|   return true; | ||||
| } | ||||
| bool DallasTemperatureSensor::check_scratch_pad() { | ||||
|   bool chksum_validity = (crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]); | ||||
|   bool config_validity = false; | ||||
|  | ||||
|   switch (this->get_address8()[0]) { | ||||
|     case DALLAS_MODEL_DS18B20: | ||||
|       config_validity = ((this->scratch_pad_[4] & 0x9F) == 0x1F); | ||||
|       break; | ||||
|     default: | ||||
|       config_validity = ((this->scratch_pad_[4] & 0x10) == 0x10); | ||||
|   } | ||||
|  | ||||
| #ifdef ESPHOME_LOG_LEVEL_VERY_VERBOSE | ||||
|   ESP_LOGVV(TAG, "Scratch pad: %02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X.%02X (%02X)", this->scratch_pad_[0], | ||||
|             this->scratch_pad_[1], this->scratch_pad_[2], this->scratch_pad_[3], this->scratch_pad_[4], | ||||
|             this->scratch_pad_[5], this->scratch_pad_[6], this->scratch_pad_[7], this->scratch_pad_[8], | ||||
|             crc8(this->scratch_pad_, 8)); | ||||
| #endif | ||||
|   return crc8(this->scratch_pad_, 8) == this->scratch_pad_[8]; | ||||
|   if (!chksum_validity) { | ||||
|     ESP_LOGW(TAG, "'%s' - Scratch pad checksum invalid!", this->get_name().c_str()); | ||||
|   } else if (!config_validity) { | ||||
|     ESP_LOGW(TAG, "'%s' - Scratch pad config register invalid!", this->get_name().c_str()); | ||||
|   } | ||||
|   return chksum_validity && config_validity; | ||||
| } | ||||
| float DallasTemperatureSensor::get_temp_c() { | ||||
|   int16_t temp = (int16_t(this->scratch_pad_[1]) << 11) | (int16_t(this->scratch_pad_[0]) << 3); | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome.components import uart | ||||
| from esphome.const import CONF_ID | ||||
| from esphome.const import CONF_ID, CONF_ADDRESS | ||||
|  | ||||
| CODEOWNERS = ["@s1lvi0"] | ||||
| DEPENDENCIES = ["uart"] | ||||
| @@ -15,7 +15,12 @@ DalyBmsComponent = daly_bms.class_( | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema({cv.GenerateID(): cv.declare_id(DalyBmsComponent)}) | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(DalyBmsComponent), | ||||
|             cv.Optional(CONF_ADDRESS, default=0x80): cv.positive_int, | ||||
|         } | ||||
|     ) | ||||
|     .extend(uart.UART_DEVICE_SCHEMA) | ||||
|     .extend(cv.polling_component_schema("30s")) | ||||
| ) | ||||
| @@ -25,3 +30,4 @@ 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) | ||||
|     cg.add(var.set_address(config[CONF_ADDRESS])) | ||||
|   | ||||
| @@ -50,7 +50,7 @@ void DalyBmsComponent::request_data_(uint8_t data_id) { | ||||
|   uint8_t request_message[DALY_FRAME_SIZE]; | ||||
|  | ||||
|   request_message[0] = 0xA5;     // Start Flag | ||||
|   request_message[1] = 0x80;     // Communication Module Address | ||||
|   request_message[1] = addr_;    // Communication Module Address | ||||
|   request_message[2] = data_id;  // Data ID | ||||
|   request_message[3] = 0x08;     // Data Length (Fixed) | ||||
|   request_message[4] = 0x00;     // Empty Data | ||||
|   | ||||
| @@ -69,11 +69,14 @@ class DalyBmsComponent : public PollingComponent, public uart::UARTDevice { | ||||
|   void update() override; | ||||
|  | ||||
|   float get_setup_priority() const override; | ||||
|   void set_address(uint8_t address) { this->addr_ = address; } | ||||
|  | ||||
|  protected: | ||||
|   void request_data_(uint8_t data_id); | ||||
|   void decode_data_(std::vector<uint8_t> data); | ||||
|  | ||||
|   uint8_t addr_; | ||||
|  | ||||
|   sensor::Sensor *voltage_sensor_{nullptr}; | ||||
|   sensor::Sensor *current_sensor_{nullptr}; | ||||
|   sensor::Sensor *battery_level_sensor_{nullptr}; | ||||
|   | ||||
| @@ -348,7 +348,7 @@ async def dfplayer_random_to_code(config, action_id, template_arg, args): | ||||
|         } | ||||
|     ), | ||||
| ) | ||||
| async def dfplyaer_is_playing_to_code(config, condition_id, template_arg, args): | ||||
| async def dfplayer_is_playing_to_code(config, condition_id, template_arg, args): | ||||
|     var = cg.new_Pvariable(condition_id, template_arg) | ||||
|     await cg.register_parented(var, config[CONF_ID]) | ||||
|     return var | ||||
|   | ||||
| @@ -591,6 +591,18 @@ void Animation::prev_frame() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Animation::set_frame(int frame) { | ||||
|   unsigned abs_frame = abs(frame); | ||||
|  | ||||
|   if (abs_frame < this->animation_frame_count_) { | ||||
|     if (frame >= 0) { | ||||
|       this->current_frame_ = frame; | ||||
|     } else { | ||||
|       this->current_frame_ = this->animation_frame_count_ - abs_frame; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} | ||||
| void DisplayPage::show() { this->parent_->show_page(this); } | ||||
| void DisplayPage::show_next() { this->next_->show(); } | ||||
|   | ||||
| @@ -85,6 +85,12 @@ enum ImageType { | ||||
|   IMAGE_TYPE_RGB565 = 4, | ||||
| }; | ||||
|  | ||||
| enum DisplayType { | ||||
|   DISPLAY_TYPE_BINARY = 1, | ||||
|   DISPLAY_TYPE_GRAYSCALE = 2, | ||||
|   DISPLAY_TYPE_COLOR = 3, | ||||
| }; | ||||
|  | ||||
| enum DisplayRotation { | ||||
|   DISPLAY_ROTATION_0_DEGREES = 0, | ||||
|   DISPLAY_ROTATION_90_DEGREES = 90, | ||||
| @@ -361,6 +367,11 @@ class DisplayBuffer { | ||||
|   virtual int get_width_internal() = 0; | ||||
|   DisplayRotation get_rotation() const { return this->rotation_; } | ||||
|  | ||||
|   /** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays, | ||||
|    * returns the type the display is currently configured to. | ||||
|    */ | ||||
|   virtual DisplayType get_display_type() = 0; | ||||
|  | ||||
|  protected: | ||||
|   void vprintf_(int x, int y, Font *font, Color color, TextAlign align, const char *format, va_list arg); | ||||
|  | ||||
| @@ -480,6 +491,12 @@ class Animation : public Image { | ||||
|   void next_frame(); | ||||
|   void prev_frame(); | ||||
|  | ||||
|   /** Selects a specific frame within the animation. | ||||
|    * | ||||
|    * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. | ||||
|    */ | ||||
|   void set_frame(int frame); | ||||
|  | ||||
|  protected: | ||||
|   int current_frame_; | ||||
|   int animation_frame_count_; | ||||
|   | ||||
| @@ -66,6 +66,9 @@ class ColorUtil { | ||||
|     } | ||||
|     return color_return; | ||||
|   } | ||||
|   static inline Color rgb332_to_color(uint8_t rgb332_color) { | ||||
|     return to_color((uint32_t) rgb332_color, COLOR_ORDER_RGB, COLOR_BITNESS_332); | ||||
|   } | ||||
|   static uint8_t color_to_332(Color color, ColorOrder color_order = ColorOrder::COLOR_ORDER_RGB) { | ||||
|     uint16_t red_color, green_color, blue_color; | ||||
|  | ||||
| @@ -100,11 +103,57 @@ class ColorUtil { | ||||
|     } | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   static uint32_t color_to_grayscale4(Color color) { | ||||
|     uint32_t gs4 = esp_scale8(color.white, 15); | ||||
|     return gs4; | ||||
|   } | ||||
|   /*** | ||||
|    * Converts a Color value to an 8bit index using a 24bit 888 palette. | ||||
|    * Uses euclidiean distance to calculate the linear distance between | ||||
|    * two points in an RGB cube, then iterates through the full palette | ||||
|    * returning the closest match. | ||||
|    * @param[in] color The target color. | ||||
|    * @param[in] palette The 256*3 byte RGB palette. | ||||
|    * @return The 8 bit index of the closest color (e.g. for display buffer). | ||||
|    */ | ||||
|   // static uint8_t color_to_index8_palette888(Color color, uint8_t *palette) { | ||||
|   static uint8_t color_to_index8_palette888(Color color, const uint8_t *palette) { | ||||
|     uint8_t closest_index = 0; | ||||
|     uint32_t minimum_dist2 = UINT32_MAX;  // Smallest distance^2 to the target | ||||
|                                           // so far | ||||
|     // int8_t(*plt)[][3] = palette; | ||||
|     int16_t tgt_r = color.r; | ||||
|     int16_t tgt_g = color.g; | ||||
|     int16_t tgt_b = color.b; | ||||
|     uint16_t x, y, z; | ||||
|     // Loop through each row of the palette | ||||
|     for (uint16_t i = 0; i < 256; i++) { | ||||
|       // Get the pallet rgb color | ||||
|       int16_t plt_r = (int16_t) palette[i * 3 + 0]; | ||||
|       int16_t plt_g = (int16_t) palette[i * 3 + 1]; | ||||
|       int16_t plt_b = (int16_t) palette[i * 3 + 2]; | ||||
|       // Calculate euclidean distance (linear distance in rgb cube). | ||||
|       x = (uint32_t) std::abs(tgt_r - plt_r); | ||||
|       y = (uint32_t) std::abs(tgt_g - plt_g); | ||||
|       z = (uint32_t) std::abs(tgt_b - plt_b); | ||||
|       uint32_t dist2 = x * x + y * y + z * z; | ||||
|       if (dist2 < minimum_dist2) { | ||||
|         minimum_dist2 = dist2; | ||||
|         closest_index = (uint8_t) i; | ||||
|       } | ||||
|     } | ||||
|     return closest_index; | ||||
|   } | ||||
|   /*** | ||||
|    * Converts an 8bit palette index (e.g. from a display buffer) to a color. | ||||
|    * @param[in] index The index to look up. | ||||
|    * @param[in] palette The 256*3 byte RGB palette. | ||||
|    * @return The RGBW Color object looked up by the palette. | ||||
|    */ | ||||
|   static Color index8_to_color_palette888(uint8_t index, const uint8_t *palette) { | ||||
|     Color color = Color(palette[index * 3 + 0], palette[index * 3 + 1], palette[index * 3 + 2], 0); | ||||
|     return color; | ||||
|   } | ||||
| }; | ||||
| }  // namespace display | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -79,10 +79,10 @@ async def to_code(config): | ||||
|     cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds)) | ||||
|     cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds)) | ||||
|  | ||||
|     cg.add_define("DSMR_GAS_MBUS_ID", config[CONF_GAS_MBUS_ID]) | ||||
|     cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID])) | ||||
|  | ||||
|     # DSMR Parser | ||||
|     cg.add_library("glmnet/Dsmr", "0.5") | ||||
|  | ||||
|     # Crypto | ||||
|     cg.add_library("rweather/Crypto", "0.2.0") | ||||
|     cg.add_library("rweather/Crypto", "0.4.0") | ||||
|   | ||||
| @@ -171,7 +171,7 @@ void Dsmr::receive_telegram_() { | ||||
|     this->telegram_[this->bytes_read_] = c; | ||||
|     this->bytes_read_++; | ||||
|  | ||||
|     // Check for a footer, i.e. exlamation mark, followed by a hex checksum. | ||||
|     // Check for a footer, i.e. exclamation mark, followed by a hex checksum. | ||||
|     if (c == '!') { | ||||
|       ESP_LOGV(TAG, "Footer of telegram found"); | ||||
|       this->footer_found_ = true; | ||||
|   | ||||
| @@ -4,9 +4,10 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| #include <map> | ||||
| #include <memory> | ||||
| #include <set> | ||||
| #include <map> | ||||
| #include <vector> | ||||
|  | ||||
| class UDP; | ||||
|  | ||||
|   | ||||
| @@ -199,7 +199,7 @@ void ENS210Component::update() { | ||||
|   }); | ||||
| } | ||||
|  | ||||
| // Extracts measurement 'data' and 'status' from a 'val' obtained from measurment. | ||||
| // Extracts measurement 'data' and 'status' from a 'val' obtained from measurement. | ||||
| void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status) { | ||||
|   *data = (val >> 0) & 0xffff; | ||||
|   int valid = (val >> 16) & 0x1; | ||||
|   | ||||
| @@ -521,6 +521,33 @@ ESP32_BOARD_PINS = { | ||||
|     }, | ||||
|     "lolin32": {"LED": 5}, | ||||
|     "lolin32_lite": {"LED": 22}, | ||||
|     "lolin_c3_mini": { | ||||
|         "TX": 21, | ||||
|         "RX": 20, | ||||
|         "SDA": 8, | ||||
|         "SCL": 10, | ||||
|         "SS": 5, | ||||
|         "MOSI": 4, | ||||
|         "MISO": 3, | ||||
|         "SCK": 2, | ||||
|         "A0": 0, | ||||
|         "A1": 1, | ||||
|         "A2": 2, | ||||
|         "A3": 3, | ||||
|         "A4": 4, | ||||
|         "A5": 5, | ||||
|         "D0": 1, | ||||
|         "D1": 10, | ||||
|         "D2": 8, | ||||
|         "D3": 7, | ||||
|         "D4": 6, | ||||
|         "D5": 2, | ||||
|         "D6": 3, | ||||
|         "D7": 4, | ||||
|         "D8": 5, | ||||
|         "LED": 7, | ||||
|         "BUTTON": 9, | ||||
|     }, | ||||
|     "lolin_d32": {"LED": 5, "_VBAT": 35}, | ||||
|     "lolin_d32_pro": {"LED": 5, "_VBAT": 35}, | ||||
|     "lopy": { | ||||
| @@ -1026,6 +1053,7 @@ BOARD_TO_VARIANT = { | ||||
|     "labplus_mpython": VARIANT_ESP32, | ||||
|     "lolin32_lite": VARIANT_ESP32, | ||||
|     "lolin32": VARIANT_ESP32, | ||||
|     "lolin_c3_mini": VARIANT_ESP32C3, | ||||
|     "lolin_d32_pro": VARIANT_ESP32, | ||||
|     "lolin_d32": VARIANT_ESP32, | ||||
|     "lopy4": VARIANT_ESP32, | ||||
|   | ||||
| @@ -42,7 +42,7 @@ def esp32_validate_gpio_pin(value): | ||||
|             "See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins", | ||||
|             value, | ||||
|         ) | ||||
|     if value in (20, 24, 28, 29, 30, 31): | ||||
|     if value in (24, 28, 29, 30, 31): | ||||
|         # These pins are not exposed in GPIO mux (reason unknown) | ||||
|         # but they're missing from IO_MUX list in datasheet | ||||
|         raise cv.Invalid(f"The pin GPIO{value} is not usable on ESP32s.") | ||||
|   | ||||
| @@ -36,6 +36,7 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { | ||||
|     save.key = key; | ||||
|     save.data.assign(data, data + len); | ||||
|     s_pending_save.emplace_back(save); | ||||
|     ESP_LOGVV(TAG, "s_pending_save: key: %s, len: %d", key.c_str(), len); | ||||
|     return true; | ||||
|   } | ||||
|   bool load(uint8_t *data, size_t len) override { | ||||
| @@ -65,6 +66,8 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { | ||||
|     if (err != 0) { | ||||
|       ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key.c_str(), esp_err_to_name(err)); | ||||
|       return false; | ||||
|     } else { | ||||
|       ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %d", key.c_str(), len); | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
| @@ -73,7 +76,6 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend { | ||||
| class ESP32Preferences : public ESPPreferences { | ||||
|  public: | ||||
|   uint32_t nvs_handle; | ||||
|   uint32_t current_offset = 0; | ||||
|  | ||||
|   void open() { | ||||
|     nvs_flash_init(); | ||||
| @@ -97,12 +99,9 @@ class ESP32Preferences : public ESPPreferences { | ||||
|   ESPPreferenceObject make_preference(size_t length, uint32_t type) override { | ||||
|     auto *pref = new ESP32PreferenceBackend();  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|     pref->nvs_handle = nvs_handle; | ||||
|     current_offset += length; | ||||
|  | ||||
|     uint32_t keyval = current_offset ^ type; | ||||
|     char keybuf[16]; | ||||
|     snprintf(keybuf, sizeof(keybuf), "%d", keyval); | ||||
|     pref->key = keybuf;  // copied to std::string | ||||
|     uint32_t keyval = type; | ||||
|     pref->key = str_sprintf("%u", keyval); | ||||
|  | ||||
|     return ESPPreferenceObject(pref); | ||||
|   } | ||||
| @@ -111,22 +110,40 @@ class ESP32Preferences : public ESPPreferences { | ||||
|     if (s_pending_save.empty()) | ||||
|       return true; | ||||
|  | ||||
|     ESP_LOGD(TAG, "Saving preferences to flash..."); | ||||
|     ESP_LOGD(TAG, "Saving %d preferences to flash...", s_pending_save.size()); | ||||
|     // goal try write all pending saves even if one fails | ||||
|     bool any_failed = false; | ||||
|     int cached = 0, written = 0, failed = 0; | ||||
|     esp_err_t last_err = ESP_OK; | ||||
|     std::string last_key{}; | ||||
|  | ||||
|     // go through vector from back to front (makes erase easier/more efficient) | ||||
|     for (ssize_t i = s_pending_save.size() - 1; i >= 0; i--) { | ||||
|       const auto &save = s_pending_save[i]; | ||||
|       esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); | ||||
|       if (err != 0) { | ||||
|         ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), | ||||
|                  esp_err_to_name(err)); | ||||
|         any_failed = true; | ||||
|         continue; | ||||
|       ESP_LOGVV(TAG, "Checking if NVS data %s has changed", save.key.c_str()); | ||||
|       if (is_changed(nvs_handle, save)) { | ||||
|         esp_err_t err = nvs_set_blob(nvs_handle, save.key.c_str(), save.data.data(), save.data.size()); | ||||
|         ESP_LOGV(TAG, "sync: key: %s, len: %d", save.key.c_str(), save.data.size()); | ||||
|         if (err != 0) { | ||||
|           ESP_LOGV(TAG, "nvs_set_blob('%s', len=%u) failed: %s", save.key.c_str(), save.data.size(), | ||||
|                    esp_err_to_name(err)); | ||||
|           failed++; | ||||
|           last_err = err; | ||||
|           last_key = save.key; | ||||
|           continue; | ||||
|         } | ||||
|         written++; | ||||
|       } else { | ||||
|         ESP_LOGV(TAG, "NVS data not changed skipping %s  len=%u", save.key.c_str(), save.data.size()); | ||||
|         cached++; | ||||
|       } | ||||
|       s_pending_save.erase(s_pending_save.begin() + i); | ||||
|     } | ||||
|     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), | ||||
|                last_key.c_str()); | ||||
|     } | ||||
|  | ||||
|     // note: commit on esp-idf currently is a no-op, nvs_set_blob always writes | ||||
|     esp_err_t err = nvs_commit(nvs_handle); | ||||
| @@ -135,7 +152,23 @@ class ESP32Preferences : public ESPPreferences { | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     return !any_failed; | ||||
|     return failed == 0; | ||||
|   } | ||||
|   bool is_changed(const uint32_t nvs_handle, const NVSData &to_save) { | ||||
|     NVSData stored_data{}; | ||||
|     size_t actual_len; | ||||
|     esp_err_t err = nvs_get_blob(nvs_handle, to_save.key.c_str(), nullptr, &actual_len); | ||||
|     if (err != 0) { | ||||
|       ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", to_save.key.c_str(), esp_err_to_name(err)); | ||||
|       return true; | ||||
|     } | ||||
|     stored_data.data.resize(actual_len); | ||||
|     err = nvs_get_blob(nvs_handle, to_save.key.c_str(), stored_data.data.data(), &actual_len); | ||||
|     if (err != 0) { | ||||
|       ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", to_save.key.c_str(), esp_err_to_name(err)); | ||||
|       return true; | ||||
|     } | ||||
|     return to_save.data != stored_data.data; | ||||
|   } | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -100,7 +100,12 @@ void ESP32BLETracker::loop() { | ||||
|           found = true; | ||||
|           if (client->state() == ClientState::DISCOVERED) { | ||||
|             esp_ble_gap_stop_scanning(); | ||||
|             if (xSemaphoreTake(this->scan_end_lock_, 10L / portTICK_PERIOD_MS)) { | ||||
| #ifdef USE_ARDUINO | ||||
|             constexpr TickType_t block_time = 10L / portTICK_PERIOD_MS; | ||||
| #else | ||||
|             constexpr TickType_t block_time = 0L;  // PR #3594 | ||||
| #endif | ||||
|             if (xSemaphoreTake(this->scan_end_lock_, block_time)) { | ||||
|               xSemaphoreGive(this->scan_end_lock_); | ||||
|             } | ||||
|           } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome import pins | ||||
| from esphome.const import ( | ||||
|     CONF_FREQUENCY, | ||||
| @@ -12,6 +13,7 @@ from esphome.const import ( | ||||
|     CONF_RESOLUTION, | ||||
|     CONF_BRIGHTNESS, | ||||
|     CONF_CONTRAST, | ||||
|     CONF_TRIGGER_ID, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
| from esphome.components.esp32 import add_idf_sdkconfig_option | ||||
| @@ -23,7 +25,14 @@ AUTO_LOAD = ["psram"] | ||||
|  | ||||
| esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera") | ||||
| ESP32Camera = esp32_camera_ns.class_("ESP32Camera", cg.PollingComponent, cg.EntityBase) | ||||
|  | ||||
| ESP32CameraStreamStartTrigger = esp32_camera_ns.class_( | ||||
|     "ESP32CameraStreamStartTrigger", | ||||
|     automation.Trigger.template(), | ||||
| ) | ||||
| ESP32CameraStreamStopTrigger = esp32_camera_ns.class_( | ||||
|     "ESP32CameraStreamStopTrigger", | ||||
|     automation.Trigger.template(), | ||||
| ) | ||||
| ESP32CameraFrameSize = esp32_camera_ns.enum("ESP32CameraFrameSize") | ||||
| FRAME_SIZES = { | ||||
|     "160X120": ESP32CameraFrameSize.ESP32_CAMERA_SIZE_160X120, | ||||
| @@ -111,6 +120,10 @@ CONF_TEST_PATTERN = "test_pattern" | ||||
| CONF_MAX_FRAMERATE = "max_framerate" | ||||
| CONF_IDLE_FRAMERATE = "idle_framerate" | ||||
|  | ||||
| # stream trigger | ||||
| CONF_ON_STREAM_START = "on_stream_start" | ||||
| CONF_ON_STREAM_STOP = "on_stream_stop" | ||||
|  | ||||
| camera_range_param = cv.int_range(min=-2, max=2) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
| @@ -178,6 +191,20 @@ CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|         cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( | ||||
|             cv.framerate, cv.Range(min=0, max=1) | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     ESP32CameraStreamStartTrigger | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     ESP32CameraStreamStopTrigger | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
| @@ -238,3 +265,11 @@ async def to_code(config): | ||||
|     if CORE.using_esp_idf: | ||||
|         cg.add_library("espressif/esp32-camera", "1.0.0") | ||||
|         add_idf_sdkconfig_option("CONFIG_RTCIO_SUPPORT_RTC_GPIO_DESC", True) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_STREAM_START, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_STREAM_STOP, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|   | ||||
| @@ -282,8 +282,20 @@ void ESP32Camera::set_idle_update_interval(uint32_t idle_update_interval) { | ||||
| void ESP32Camera::add_image_callback(std::function<void(std::shared_ptr<CameraImage>)> &&f) { | ||||
|   this->new_image_callback_.add(std::move(f)); | ||||
| } | ||||
| void ESP32Camera::start_stream(CameraRequester requester) { this->stream_requesters_ |= (1U << requester); } | ||||
| void ESP32Camera::stop_stream(CameraRequester requester) { this->stream_requesters_ &= ~(1U << requester); } | ||||
| void ESP32Camera::add_stream_start_callback(std::function<void()> &&callback) { | ||||
|   this->stream_start_callback_.add(std::move(callback)); | ||||
| } | ||||
| void ESP32Camera::add_stream_stop_callback(std::function<void()> &&callback) { | ||||
|   this->stream_stop_callback_.add(std::move(callback)); | ||||
| } | ||||
| void ESP32Camera::start_stream(CameraRequester requester) { | ||||
|   this->stream_start_callback_.call(); | ||||
|   this->stream_requesters_ |= (1U << requester); | ||||
| } | ||||
| void ESP32Camera::stop_stream(CameraRequester requester) { | ||||
|   this->stream_stop_callback_.call(); | ||||
|   this->stream_requesters_ &= ~(1U << requester); | ||||
| } | ||||
| void ESP32Camera::request_image(CameraRequester requester) { this->single_requesters_ |= (1U << requester); } | ||||
| void ESP32Camera::update_camera_parameters() { | ||||
|   sensor_t *s = esp_camera_sensor_get(); | ||||
| @@ -305,12 +317,11 @@ void ESP32Camera::update_camera_parameters() { | ||||
|   s->set_gainceiling(s, (gainceiling_t) this->agc_gain_ceiling_); | ||||
|   /* update white balance mode */ | ||||
|   s->set_wb_mode(s, (int) this->wb_mode_);  // 0 to 4 | ||||
|   /* update test patern */ | ||||
|   /* update test pattern */ | ||||
|   s->set_colorbar(s, this->test_pattern_); | ||||
| } | ||||
|  | ||||
| /* ---------------- Internal methods ---------------- */ | ||||
| uint32_t ESP32Camera::hash_base() { return 3010542557UL; } | ||||
| bool ESP32Camera::has_requested_image_() const { return this->single_requesters_ || this->stream_requesters_; } | ||||
| bool ESP32Camera::can_return_image_() const { return this->current_image_.use_count() == 1; } | ||||
| void ESP32Camera::framebuffer_task(void *pv) { | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/entity_base.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| @@ -145,9 +146,11 @@ class ESP32Camera : public Component, public EntityBase { | ||||
|   void request_image(CameraRequester requester); | ||||
|   void update_camera_parameters(); | ||||
|  | ||||
|   void add_stream_start_callback(std::function<void()> &&callback); | ||||
|   void add_stream_stop_callback(std::function<void()> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   /* internal methods */ | ||||
|   uint32_t hash_base() override; | ||||
|   bool has_requested_image_() const; | ||||
|   bool can_return_image_() const; | ||||
|  | ||||
| @@ -187,6 +190,8 @@ class ESP32Camera : public Component, public EntityBase { | ||||
|   QueueHandle_t framebuffer_get_queue_; | ||||
|   QueueHandle_t framebuffer_return_queue_; | ||||
|   CallbackManager<void(std::shared_ptr<CameraImage>)> new_image_callback_; | ||||
|   CallbackManager<void()> stream_start_callback_{}; | ||||
|   CallbackManager<void()> stream_stop_callback_{}; | ||||
|  | ||||
|   uint32_t last_idle_request_{0}; | ||||
|   uint32_t last_update_{0}; | ||||
| @@ -195,6 +200,23 @@ class ESP32Camera : public Component, public EntityBase { | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
| extern ESP32Camera *global_esp32_camera; | ||||
|  | ||||
| class ESP32CameraStreamStartTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit ESP32CameraStreamStartTrigger(ESP32Camera *parent) { | ||||
|     parent->add_stream_start_callback([this]() { this->trigger(); }); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| class ESP32CameraStreamStopTrigger : public Trigger<> { | ||||
|  public: | ||||
|   explicit ESP32CameraStreamStopTrigger(ESP32Camera *parent) { | ||||
|     parent->add_stream_stop_callback([this]() { this->trigger(); }); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_camera | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| #include "fan.h" | ||||
| #include "fan_helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -61,22 +60,6 @@ void FanCall::validate_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| // This whole method is deprecated, don't warn about usage of deprecated methods inside of it. | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
| FanCall &FanCall::set_speed(const char *legacy_speed) { | ||||
|   const auto supported_speed_count = this->parent_.get_traits().supported_speed_count(); | ||||
|   if (strcasecmp(legacy_speed, "low") == 0) { | ||||
|     this->set_speed(fan::speed_enum_to_level(FAN_SPEED_LOW, supported_speed_count)); | ||||
|   } else if (strcasecmp(legacy_speed, "medium") == 0) { | ||||
|     this->set_speed(fan::speed_enum_to_level(FAN_SPEED_MEDIUM, supported_speed_count)); | ||||
|   } else if (strcasecmp(legacy_speed, "high") == 0) { | ||||
|     this->set_speed(fan::speed_enum_to_level(FAN_SPEED_HIGH, supported_speed_count)); | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
| #pragma GCC diagnostic pop | ||||
|  | ||||
| FanCall FanRestoreState::to_call(Fan &fan) { | ||||
|   auto call = fan.make_call(); | ||||
|   call.set_state(this->state); | ||||
| @@ -169,7 +152,6 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { | ||||
|   if (this->get_traits().supports_direction()) | ||||
|     ESP_LOGCONFIG(tag, "%s  Direction: YES", prefix); | ||||
| } | ||||
| uint32_t Fan::hash_base() { return 418001110UL; } | ||||
|  | ||||
| }  // namespace fan | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -16,13 +16,6 @@ namespace fan { | ||||
|     (obj)->dump_traits_(TAG, prefix); \ | ||||
|   } | ||||
|  | ||||
| /// Simple enum to represent the speed of a fan. - DEPRECATED - Will be deleted soon | ||||
| enum ESPDEPRECATED("FanSpeed is deprecated.", "2021.9") FanSpeed { | ||||
|   FAN_SPEED_LOW = 0,     ///< The fan is running on low speed. | ||||
|   FAN_SPEED_MEDIUM = 1,  ///< The fan is running on medium speed. | ||||
|   FAN_SPEED_HIGH = 2     ///< The fan is running on high/full speed. | ||||
| }; | ||||
|  | ||||
| /// Simple enum to represent the direction of a fan. | ||||
| enum class FanDirection { FORWARD = 0, REVERSE = 1 }; | ||||
|  | ||||
| @@ -143,7 +136,6 @@ class Fan : public EntityBase { | ||||
|   void save_state_(); | ||||
|  | ||||
|   void dump_traits_(const char *tag, const char *prefix); | ||||
|   uint32_t hash_base() override; | ||||
|  | ||||
|   CallbackManager<void()> state_callback_{}; | ||||
|   ESPPreferenceObject rtc_; | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| #include <cassert> | ||||
| #include "fan_helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace fan { | ||||
|  | ||||
| // This whole file is deprecated, don't warn about usage of deprecated types in here. | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
|  | ||||
| FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels) { | ||||
|   const auto speed_ratio = static_cast<float>(speed_level) / (supported_speed_levels + 1); | ||||
|   const auto legacy_level = clamp<int>(static_cast<int>(ceilf(speed_ratio * 3)), 1, 3); | ||||
|   return static_cast<FanSpeed>(legacy_level - 1); | ||||
| } | ||||
|  | ||||
| int speed_enum_to_level(FanSpeed speed, int supported_speed_levels) { | ||||
|   const auto enum_level = static_cast<int>(speed) + 1; | ||||
|   const auto speed_level = roundf(enum_level / 3.0f * supported_speed_levels); | ||||
|   return static_cast<int>(speed_level); | ||||
| } | ||||
|  | ||||
| }  // namespace fan | ||||
| }  // namespace esphome | ||||
| @@ -1,20 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "fan.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace fan { | ||||
|  | ||||
| // Shut-up about usage of deprecated FanSpeed for a bit. | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
|  | ||||
| ESPDEPRECATED("FanSpeed and speed_level_to_enum() are deprecated.", "2021.9") | ||||
| FanSpeed speed_level_to_enum(int speed_level, int supported_speed_levels); | ||||
| ESPDEPRECATED("FanSpeed and speed_enum_to_level() are deprecated.", "2021.9") | ||||
| int speed_enum_to_level(FanSpeed speed, int supported_speed_levels); | ||||
|  | ||||
| #pragma GCC diagnostic pop | ||||
|  | ||||
| }  // namespace fan | ||||
| }  // namespace esphome | ||||
							
								
								
									
										1
									
								
								esphome/components/feedback/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/feedback/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CODEOWNERS = ["@ianchi"] | ||||
							
								
								
									
										157
									
								
								esphome/components/feedback/cover.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/feedback/cover.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,157 @@ | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.components import binary_sensor, cover | ||||
| from esphome.const import ( | ||||
|     CONF_ASSUMED_STATE, | ||||
|     CONF_CLOSE_ACTION, | ||||
|     CONF_CLOSE_DURATION, | ||||
|     CONF_CLOSE_ENDSTOP, | ||||
|     CONF_ID, | ||||
|     CONF_OPEN_ACTION, | ||||
|     CONF_OPEN_DURATION, | ||||
|     CONF_OPEN_ENDSTOP, | ||||
|     CONF_STOP_ACTION, | ||||
|     CONF_MAX_DURATION, | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
|  | ||||
| CONF_OPEN_SENSOR = "open_sensor" | ||||
| CONF_CLOSE_SENSOR = "close_sensor" | ||||
| CONF_OPEN_OBSTACLE_SENSOR = "open_obstacle_sensor" | ||||
| CONF_CLOSE_OBSTACLE_SENSOR = "close_obstacle_sensor" | ||||
| CONF_HAS_BUILT_IN_ENDSTOP = "has_built_in_endstop" | ||||
| CONF_INFER_ENDSTOP_FROM_MOVEMENT = "infer_endstop_from_movement" | ||||
| CONF_DIRECTION_CHANGE_WAIT_TIME = "direction_change_wait_time" | ||||
| CONF_ACCELERATION_WAIT_TIME = "acceleration_wait_time" | ||||
| CONF_OBSTACLE_ROLLBACK = "obstacle_rollback" | ||||
|  | ||||
| endstop_ns = cg.esphome_ns.namespace("feedback") | ||||
| FeedbackCover = endstop_ns.class_("FeedbackCover", cover.Cover, cg.Component) | ||||
|  | ||||
|  | ||||
| def validate_infer_endstop(config): | ||||
|     if config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] is True: | ||||
|         if config[CONF_HAS_BUILT_IN_ENDSTOP] is False: | ||||
|             raise cv.Invalid( | ||||
|                 f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} can only be set if {CONF_HAS_BUILT_IN_ENDSTOP} is also set" | ||||
|             ) | ||||
|  | ||||
|         if CONF_OPEN_SENSOR not in config: | ||||
|             raise cv.Invalid( | ||||
|                 f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if movement sensors are not supplied" | ||||
|             ) | ||||
|  | ||||
|         if CONF_OPEN_ENDSTOP in config or CONF_CLOSE_ENDSTOP in config: | ||||
|             raise cv.Invalid( | ||||
|                 f"{CONF_INFER_ENDSTOP_FROM_MOVEMENT} cannot be set if endstop sensors are supplied" | ||||
|             ) | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_FEEDBACK_COVER_BASE_SCHEMA = cover.COVER_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(FeedbackCover), | ||||
|         cv.Required(CONF_STOP_ACTION): automation.validate_automation(single=True), | ||||
|         cv.Required(CONF_OPEN_ACTION): automation.validate_automation(single=True), | ||||
|         cv.Required(CONF_OPEN_DURATION): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_OPEN_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Optional(CONF_OPEN_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Optional(CONF_OPEN_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Required(CONF_CLOSE_ACTION): automation.validate_automation(single=True), | ||||
|         cv.Required(CONF_CLOSE_DURATION): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_CLOSE_ENDSTOP): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Optional(CONF_CLOSE_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Optional(CONF_CLOSE_OBSTACLE_SENSOR): cv.use_id(binary_sensor.BinarySensor), | ||||
|         cv.Optional(CONF_MAX_DURATION): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_HAS_BUILT_IN_ENDSTOP, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_ASSUMED_STATE): cv.boolean, | ||||
|         cv.Optional( | ||||
|             CONF_UPDATE_INTERVAL, "1000ms" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_INFER_ENDSTOP_FROM_MOVEMENT, False): cv.boolean, | ||||
|         cv.Optional( | ||||
|             CONF_DIRECTION_CHANGE_WAIT_TIME | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional( | ||||
|             CONF_ACCELERATION_WAIT_TIME, "0s" | ||||
|         ): cv.positive_time_period_milliseconds, | ||||
|         cv.Optional(CONF_OBSTACLE_ROLLBACK, default="10%"): cv.percentage, | ||||
|     }, | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     CONFIG_FEEDBACK_COVER_BASE_SCHEMA, | ||||
|     cv.has_none_or_all_keys(CONF_OPEN_SENSOR, CONF_CLOSE_SENSOR), | ||||
|     validate_infer_endstop, | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await cover.register_cover(var, config) | ||||
|  | ||||
|     # STOP | ||||
|     await automation.build_automation( | ||||
|         var.get_stop_trigger(), [], config[CONF_STOP_ACTION] | ||||
|     ) | ||||
|  | ||||
|     # OPEN | ||||
|     await automation.build_automation( | ||||
|         var.get_open_trigger(), [], config[CONF_OPEN_ACTION] | ||||
|     ) | ||||
|     cg.add(var.set_open_duration(config[CONF_OPEN_DURATION])) | ||||
|     if CONF_OPEN_ENDSTOP in config: | ||||
|         bin = await cg.get_variable(config[CONF_OPEN_ENDSTOP]) | ||||
|         cg.add(var.set_open_endstop(bin)) | ||||
|     if CONF_OPEN_SENSOR in config: | ||||
|         bin = await cg.get_variable(config[CONF_OPEN_SENSOR]) | ||||
|         cg.add(var.set_open_sensor(bin)) | ||||
|     if CONF_OPEN_OBSTACLE_SENSOR in config: | ||||
|         bin = await cg.get_variable(config[CONF_OPEN_OBSTACLE_SENSOR]) | ||||
|         cg.add(var.set_open_obstacle_sensor(bin)) | ||||
|  | ||||
|     # CLOSE | ||||
|     await automation.build_automation( | ||||
|         var.get_close_trigger(), [], config[CONF_CLOSE_ACTION] | ||||
|     ) | ||||
|     cg.add(var.set_close_duration(config[CONF_CLOSE_DURATION])) | ||||
|     if CONF_CLOSE_ENDSTOP in config: | ||||
|         bin = await cg.get_variable(config[CONF_CLOSE_ENDSTOP]) | ||||
|         cg.add(var.set_close_endstop(bin)) | ||||
|     if CONF_CLOSE_SENSOR in config: | ||||
|         bin = await cg.get_variable(config[CONF_CLOSE_SENSOR]) | ||||
|         cg.add(var.set_close_sensor(bin)) | ||||
|     if CONF_CLOSE_OBSTACLE_SENSOR in config: | ||||
|         bin = await cg.get_variable(config[CONF_CLOSE_OBSTACLE_SENSOR]) | ||||
|         cg.add(var.set_close_obstacle_sensor(bin)) | ||||
|  | ||||
|     # OTHER | ||||
|     if CONF_MAX_DURATION in config: | ||||
|         cg.add(var.set_max_duration(config[CONF_MAX_DURATION])) | ||||
|  | ||||
|     cg.add(var.set_has_built_in_endstop(config[CONF_HAS_BUILT_IN_ENDSTOP])) | ||||
|  | ||||
|     if CONF_ASSUMED_STATE in config: | ||||
|         cg.add(var.set_assumed_state(config[CONF_ASSUMED_STATE])) | ||||
|     else: | ||||
|         cg.add( | ||||
|             var.set_assumed_state( | ||||
|                 not ( | ||||
|                     (CONF_CLOSE_ENDSTOP in config and CONF_OPEN_ENDSTOP in config) | ||||
|                     or config[CONF_INFER_ENDSTOP_FROM_MOVEMENT] | ||||
|                 ) | ||||
|             ) | ||||
|         ) | ||||
|  | ||||
|     cg.add(var.set_update_interval(config[CONF_UPDATE_INTERVAL])) | ||||
|     cg.add(var.set_infer_endstop(config[CONF_INFER_ENDSTOP_FROM_MOVEMENT])) | ||||
|     if CONF_DIRECTION_CHANGE_WAIT_TIME in config: | ||||
|         cg.add( | ||||
|             var.set_direction_change_waittime(config[CONF_DIRECTION_CHANGE_WAIT_TIME]) | ||||
|         ) | ||||
|     cg.add(var.set_acceleration_wait_time(config[CONF_ACCELERATION_WAIT_TIME])) | ||||
|     cg.add(var.set_obstacle_rollback(config[CONF_OBSTACLE_ROLLBACK])) | ||||
							
								
								
									
										445
									
								
								esphome/components/feedback/feedback_cover.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								esphome/components/feedback/feedback_cover.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,445 @@ | ||||
| #include "feedback_cover.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace feedback { | ||||
|  | ||||
| static const char *const TAG = "feedback.cover"; | ||||
|  | ||||
| using namespace esphome::cover; | ||||
|  | ||||
| void FeedbackCover::setup() { | ||||
|   auto restore = this->restore_state_(); | ||||
|  | ||||
|   if (restore.has_value()) { | ||||
|     restore->apply(this); | ||||
|   } else { | ||||
|     // if no other information, assume half open | ||||
|     this->position = 0.5f; | ||||
|   } | ||||
|   this->current_operation = COVER_OPERATION_IDLE; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   // if available, get position from endstop sensors | ||||
|   if (this->open_endstop_ != nullptr && this->open_endstop_->state) { | ||||
|     this->position = COVER_OPEN; | ||||
|   } else if (this->close_endstop_ != nullptr && this->close_endstop_->state) { | ||||
|     this->position = COVER_CLOSED; | ||||
|   } | ||||
|  | ||||
|   // if available, get moving state from sensors | ||||
|   if (this->open_feedback_ != nullptr && this->open_feedback_->state) { | ||||
|     this->current_operation = COVER_OPERATION_OPENING; | ||||
|   } else if (this->close_feedback_ != nullptr && this->close_feedback_->state) { | ||||
|     this->current_operation = COVER_OPERATION_CLOSING; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   this->last_recompute_time_ = this->start_dir_time_ = millis(); | ||||
| } | ||||
|  | ||||
| CoverTraits FeedbackCover::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_supports_position(true); | ||||
|   traits.set_supports_toggle(true); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| void FeedbackCover::dump_config() { | ||||
|   LOG_COVER("", "Endstop Cover", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Open Duration: %.1fs", this->open_duration_ / 1e3f); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   LOG_BINARY_SENSOR("  ", "Open Endstop", this->open_endstop_); | ||||
|   LOG_BINARY_SENSOR("  ", "Open Feedback", this->open_feedback_); | ||||
|   LOG_BINARY_SENSOR("  ", "Open Obstacle", this->open_obstacle_); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "  Close Duration: %.1fs", this->close_duration_ / 1e3f); | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   LOG_BINARY_SENSOR("  ", "Close Endstop", this->close_endstop_); | ||||
|   LOG_BINARY_SENSOR("  ", "Close Feedback", this->close_feedback_); | ||||
|   LOG_BINARY_SENSOR("  ", "Close Obstacle", this->close_obstacle_); | ||||
| #endif | ||||
|   if (this->has_built_in_endstop_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Has builtin endstop: YES"); | ||||
|   } | ||||
|   if (this->infer_endstop_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Infer endstop from movement: YES"); | ||||
|   } | ||||
|   if (this->max_duration_ < UINT32_MAX) { | ||||
|     ESP_LOGCONFIG(TAG, "  Max Duration: %.1fs", this->max_duration_ / 1e3f); | ||||
|   } | ||||
|   if (this->direction_change_waittime_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Direction change wait time: %.1fs", *this->direction_change_waittime_ / 1e3f); | ||||
|   } | ||||
|   if (this->acceleration_wait_time_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Acceleration wait time: %.1fs", this->acceleration_wait_time_ / 1e3f); | ||||
|   } | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   if (this->obstacle_rollback_ && (this->open_obstacle_ != nullptr || this->close_obstacle_ != nullptr)) { | ||||
|     ESP_LOGCONFIG(TAG, "  Obstacle rollback: %.1f%%", this->obstacle_rollback_ * 100); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|  | ||||
| void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback) { | ||||
|   this->open_feedback_ = open_feedback; | ||||
|  | ||||
|   // setup callbacks to react to sensor changes | ||||
|   open_feedback->add_on_state_callback([this](bool state) { | ||||
|     ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); | ||||
|     this->recompute_position_(); | ||||
|     if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) { | ||||
|       this->endstop_reached_(true); | ||||
|     } | ||||
|     this->set_current_operation_(state ? COVER_OPERATION_OPENING : COVER_OPERATION_IDLE, false); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback) { | ||||
|   this->close_feedback_ = close_feedback; | ||||
|  | ||||
|   close_feedback->add_on_state_callback([this](bool state) { | ||||
|     ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED"); | ||||
|     this->recompute_position_(); | ||||
|     if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) { | ||||
|       this->endstop_reached_(false); | ||||
|     } | ||||
|  | ||||
|     this->set_current_operation_(state ? COVER_OPERATION_CLOSING : COVER_OPERATION_IDLE, false); | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void FeedbackCover::set_open_endstop(binary_sensor::BinarySensor *open_endstop) { | ||||
|   this->open_endstop_ = open_endstop; | ||||
|   open_endstop->add_on_state_callback([this](bool state) { | ||||
|     if (state) { | ||||
|       this->endstop_reached_(true); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop) { | ||||
|   this->close_endstop_ = close_endstop; | ||||
|   close_endstop->add_on_state_callback([this](bool state) { | ||||
|     if (state) { | ||||
|       this->endstop_reached_(false); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void FeedbackCover::endstop_reached_(bool open_endstop) { | ||||
|   const uint32_t now = millis(); | ||||
|  | ||||
|   this->position = open_endstop ? COVER_OPEN : COVER_CLOSED; | ||||
|  | ||||
|   // only act if endstop activated while moving in the right direction, in case we are coming back | ||||
|   // from a position slightly past the endpoint | ||||
|   if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) { | ||||
|     float dur = (now - this->start_dir_time_) / 1e3f; | ||||
|     ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur); | ||||
|  | ||||
|     // if there is no external mechanism, stop the cover | ||||
|     if (!this->has_built_in_endstop_) { | ||||
|       this->start_direction_(COVER_OPERATION_IDLE); | ||||
|     } else { | ||||
|       this->set_current_operation_(COVER_OPERATION_IDLE, true); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // always sync position and publish | ||||
|   this->publish_state(); | ||||
|   this->last_publish_time_ = now; | ||||
| } | ||||
|  | ||||
| void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool is_triggered) { | ||||
|   if (is_triggered) { | ||||
|     this->current_trigger_operation_ = operation; | ||||
|   } | ||||
|  | ||||
|   // if it is setting the actual operation (not triggered one) or | ||||
|   // if we don't have moving sensor, we operate in optimistic mode, assuming actions take place immediately | ||||
|   // thus, triggered operation always sets current operation. | ||||
|   // otherwise, current operation comes from sensor, and may differ from requested operation | ||||
|   // this might be from delays or complex actions, or because the movement was not trigger by the component | ||||
|   // but initiated externally | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr)) | ||||
| #endif | ||||
|   { | ||||
|     auto now = millis(); | ||||
|     this->current_operation = operation; | ||||
|     this->start_dir_time_ = this->last_recompute_time_ = now; | ||||
|     this->publish_state(); | ||||
|     this->last_publish_time_ = now; | ||||
|   } | ||||
| } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| void FeedbackCover::set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle) { | ||||
|   this->close_obstacle_ = close_obstacle; | ||||
|  | ||||
|   close_obstacle->add_on_state_callback([this](bool state) { | ||||
|     if (state && (this->current_operation == COVER_OPERATION_CLOSING || | ||||
|                   this->current_trigger_operation_ == COVER_OPERATION_CLOSING)) { | ||||
|       ESP_LOGD(TAG, "'%s' - Close obstacle detected.", this->name_.c_str()); | ||||
|       this->start_direction_(COVER_OPERATION_IDLE); | ||||
|  | ||||
|       if (this->obstacle_rollback_) { | ||||
|         this->target_position_ = clamp(this->position + this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); | ||||
|         this->start_direction_(COVER_OPERATION_OPENING); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| void FeedbackCover::set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle) { | ||||
|   this->open_obstacle_ = open_obstacle; | ||||
|  | ||||
|   open_obstacle->add_on_state_callback([this](bool state) { | ||||
|     if (state && (this->current_operation == COVER_OPERATION_OPENING || | ||||
|                   this->current_trigger_operation_ == COVER_OPERATION_OPENING)) { | ||||
|       ESP_LOGD(TAG, "'%s' - Open obstacle detected.", this->name_.c_str()); | ||||
|       this->start_direction_(COVER_OPERATION_IDLE); | ||||
|  | ||||
|       if (this->obstacle_rollback_) { | ||||
|         this->target_position_ = clamp(this->position - this->obstacle_rollback_, COVER_CLOSED, COVER_OPEN); | ||||
|         this->start_direction_(COVER_OPERATION_CLOSING); | ||||
|       } | ||||
|     } | ||||
|   }); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void FeedbackCover::loop() { | ||||
|   if (this->current_operation == COVER_OPERATION_IDLE) | ||||
|     return; | ||||
|   const uint32_t now = millis(); | ||||
|  | ||||
|   // Recompute position every loop cycle | ||||
|   this->recompute_position_(); | ||||
|  | ||||
|   // if we initiated the move, check if we reached position or max time | ||||
|   // (stoping from endstop sensor is handled in callback) | ||||
|   if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { | ||||
|     if (this->is_at_target_()) { | ||||
|       if (this->has_built_in_endstop_ && | ||||
|           (this->target_position_ == COVER_OPEN || this->target_position_ == COVER_CLOSED)) { | ||||
|         // Don't trigger stop, let the cover stop by itself. | ||||
|         this->set_current_operation_(COVER_OPERATION_IDLE, true); | ||||
|       } else { | ||||
|         this->start_direction_(COVER_OPERATION_IDLE); | ||||
|       } | ||||
|     } else if (now - this->start_dir_time_ > this->max_duration_) { | ||||
|       ESP_LOGD(TAG, "'%s' - Max duration reached. Stopping cover.", this->name_.c_str()); | ||||
|       this->start_direction_(COVER_OPERATION_IDLE); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // update current position at requested interval, regardless of who started the movement | ||||
|   // so that we also update UI if there was an external movement | ||||
|   // don´t save intermediate positions | ||||
|   if (now - this->last_publish_time_ > this->update_interval_) { | ||||
|     this->publish_state(false); | ||||
|     this->last_publish_time_ = now; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void FeedbackCover::control(const CoverCall &call) { | ||||
|   // stop action logic | ||||
|   if (call.get_stop()) { | ||||
|     this->start_direction_(COVER_OPERATION_IDLE); | ||||
|   } else if (call.get_toggle().has_value()) { | ||||
|     // toggle action logic: OPEN - STOP - CLOSE | ||||
|     if (this->current_trigger_operation_ != COVER_OPERATION_IDLE) { | ||||
|       this->start_direction_(COVER_OPERATION_IDLE); | ||||
|     } else { | ||||
|       if (this->position == COVER_CLOSED || this->last_operation_ == COVER_OPERATION_CLOSING) { | ||||
|         this->target_position_ = COVER_OPEN; | ||||
|         this->start_direction_(COVER_OPERATION_OPENING); | ||||
|       } else { | ||||
|         this->target_position_ = COVER_CLOSED; | ||||
|         this->start_direction_(COVER_OPERATION_CLOSING); | ||||
|       } | ||||
|     } | ||||
|   } else if (call.get_position().has_value()) { | ||||
|     // go to position action | ||||
|     auto pos = *call.get_position(); | ||||
|     if (pos == this->position) { | ||||
|       // already at target, | ||||
|  | ||||
|       // for covers with built in end stop, if we don´t have sensors we should send the command again | ||||
|       // to make sure the assumed state is not wrong | ||||
|       if (this->has_built_in_endstop_ && ((pos == COVER_OPEN | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|                                            && this->open_endstop_ == nullptr | ||||
| #endif | ||||
|                                            && !this->infer_endstop_) || | ||||
|                                           (pos == COVER_CLOSED | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|                                            && this->close_endstop_ == nullptr | ||||
| #endif | ||||
|                                            && !this->infer_endstop_))) { | ||||
|         this->target_position_ = pos; | ||||
|         this->start_direction_(pos == COVER_CLOSED ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); | ||||
|       } else if (this->current_operation != COVER_OPERATION_IDLE || | ||||
|                  this->current_trigger_operation_ != COVER_OPERATION_IDLE) { | ||||
|         // if we are moving, stop | ||||
|         this->start_direction_(COVER_OPERATION_IDLE); | ||||
|       } | ||||
|     } else { | ||||
|       this->target_position_ = pos; | ||||
|       this->start_direction_(pos < this->position ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void FeedbackCover::stop_prev_trigger_() { | ||||
|   if (this->direction_change_waittime_.has_value()) { | ||||
|     this->cancel_timeout("direction_change"); | ||||
|   } | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| bool FeedbackCover::is_at_target_() const { | ||||
|   // if initiated externally, current operation might be different from | ||||
|   // operation that was triggered, thus evaluate position against what was asked | ||||
|  | ||||
|   switch (this->current_trigger_operation_) { | ||||
|     case COVER_OPERATION_OPENING: | ||||
|       return this->position >= this->target_position_; | ||||
|     case COVER_OPERATION_CLOSING: | ||||
|       return this->position <= this->target_position_; | ||||
|     case COVER_OPERATION_IDLE: | ||||
|       return this->current_operation == COVER_OPERATION_IDLE; | ||||
|     default: | ||||
|       return true; | ||||
|   } | ||||
| } | ||||
| void FeedbackCover::start_direction_(CoverOperation dir) { | ||||
|   Trigger<> *trig; | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   binary_sensor::BinarySensor *obstacle{nullptr}; | ||||
| #endif | ||||
|  | ||||
|   switch (dir) { | ||||
|     case COVER_OPERATION_IDLE: | ||||
|       trig = this->stop_trigger_; | ||||
|       break; | ||||
|     case COVER_OPERATION_OPENING: | ||||
|       this->last_operation_ = dir; | ||||
|       trig = this->open_trigger_; | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|       obstacle = this->open_obstacle_; | ||||
| #endif | ||||
|       break; | ||||
|     case COVER_OPERATION_CLOSING: | ||||
|       this->last_operation_ = dir; | ||||
|       trig = this->close_trigger_; | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|       obstacle = this->close_obstacle_; | ||||
| #endif | ||||
|       break; | ||||
|     default: | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   this->stop_prev_trigger_(); | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   // check if there is an obstacle to start the new operation -> abort without any change | ||||
|   // the case when an obstacle appears while moving is handled in the callback | ||||
|   if (obstacle != nullptr && obstacle->state) { | ||||
|     ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(), | ||||
|              dir == COVER_OPERATION_OPENING ? "Open" : "Close"); | ||||
|     return; | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   // if we are moving and need to move in the opposite direction | ||||
|   // check if we have a wait time | ||||
|   if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE && | ||||
|       this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) { | ||||
|     ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str()); | ||||
|     this->start_direction_(COVER_OPERATION_IDLE); | ||||
|  | ||||
|     this->set_timeout("direction_change", *this->direction_change_waittime_, | ||||
|                       [this, dir]() { this->start_direction_(dir); }); | ||||
|  | ||||
|   } else { | ||||
|     this->set_current_operation_(dir, true); | ||||
|     this->prev_command_trigger_ = trig; | ||||
|     ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(), | ||||
|              dir == COVER_OPERATION_OPENING   ? "OPEN" | ||||
|              : dir == COVER_OPERATION_CLOSING ? "CLOSE" | ||||
|                                               : "STOP"); | ||||
|     trig->trigger(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void FeedbackCover::recompute_position_() { | ||||
|   if (this->current_operation == COVER_OPERATION_IDLE) | ||||
|     return; | ||||
|  | ||||
|   const uint32_t now = millis(); | ||||
|   float dir; | ||||
|   float action_dur; | ||||
|   float min_pos; | ||||
|   float max_pos; | ||||
|  | ||||
|   // endstop sensors update position from their callbacks, and sets the fully open/close value | ||||
|   // If we have endstop, estimation never reaches the fully open/closed state. | ||||
|   // but if movement continues past corresponding endstop (inertia), keep the fully open/close state | ||||
|  | ||||
|   switch (this->current_operation) { | ||||
|     case COVER_OPERATION_OPENING: | ||||
|       dir = 1.0f; | ||||
|       action_dur = this->open_duration_; | ||||
|       min_pos = COVER_CLOSED; | ||||
|       max_pos = ( | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|                     this->open_endstop_ != nullptr || | ||||
| #endif | ||||
|                     this->infer_endstop_) && | ||||
|                         this->position < COVER_OPEN | ||||
|                     ? 0.99f | ||||
|                     : COVER_OPEN; | ||||
|       break; | ||||
|     case COVER_OPERATION_CLOSING: | ||||
|       dir = -1.0f; | ||||
|       action_dur = this->close_duration_; | ||||
|       min_pos = ( | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|                     this->close_endstop_ != nullptr || | ||||
| #endif | ||||
|                     this->infer_endstop_) && | ||||
|                         this->position > COVER_CLOSED | ||||
|                     ? 0.01f | ||||
|                     : COVER_CLOSED; | ||||
|       max_pos = COVER_OPEN; | ||||
|       break; | ||||
|     default: | ||||
|       return; | ||||
|   } | ||||
|  | ||||
|   // check if we have an acceleration_wait_time, and remove from position computation | ||||
|   if (now > (this->start_dir_time_ + this->acceleration_wait_time_)) { | ||||
|     this->position += | ||||
|         dir * (now - std::max(this->start_dir_time_ + this->acceleration_wait_time_, this->last_recompute_time_)) / | ||||
|         (action_dur - this->acceleration_wait_time_); | ||||
|     this->position = clamp(this->position, min_pos, max_pos); | ||||
|   } | ||||
|   this->last_recompute_time_ = now; | ||||
| } | ||||
|  | ||||
| }  // namespace feedback | ||||
| }  // namespace esphome | ||||
							
								
								
									
										90
									
								
								esphome/components/feedback/feedback_cover.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								esphome/components/feedback/feedback_cover.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,90 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
| #endif | ||||
| #include "esphome/components/cover/cover.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace feedback { | ||||
|  | ||||
| class FeedbackCover : public cover::Cover, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::DATA; }; | ||||
|  | ||||
|   Trigger<> *get_open_trigger() const { return this->open_trigger_; } | ||||
|   Trigger<> *get_close_trigger() const { return this->close_trigger_; } | ||||
|   Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   void set_open_endstop(binary_sensor::BinarySensor *open_endstop); | ||||
|   void set_open_sensor(binary_sensor::BinarySensor *open_feedback); | ||||
|   void set_open_obstacle_sensor(binary_sensor::BinarySensor *open_obstacle); | ||||
|   void set_close_endstop(binary_sensor::BinarySensor *close_endstop); | ||||
|   void set_close_sensor(binary_sensor::BinarySensor *close_feedback); | ||||
|   void set_close_obstacle_sensor(binary_sensor::BinarySensor *close_obstacle); | ||||
| #endif | ||||
|   void set_open_duration(uint32_t duration) { this->open_duration_ = duration; } | ||||
|   void set_close_duration(uint32_t duration) { this->close_duration_ = duration; } | ||||
|   void set_has_built_in_endstop(bool value) { this->has_built_in_endstop_ = value; } | ||||
|   void set_assumed_state(bool value) { this->assumed_state_ = value; } | ||||
|   void set_max_duration(uint32_t max_duration) { this->max_duration_ = max_duration; } | ||||
|   void set_obstacle_rollback(float obstacle_rollback) { this->obstacle_rollback_ = obstacle_rollback; } | ||||
|   void set_update_interval(uint32_t interval) { this->update_interval_ = interval; } | ||||
|   void set_infer_endstop(bool infer_endstop) { this->infer_endstop_ = infer_endstop; } | ||||
|   void set_direction_change_waittime(uint32_t waittime) { this->direction_change_waittime_ = waittime; } | ||||
|   void set_acceleration_wait_time(uint32_t waittime) { this->acceleration_wait_time_ = waittime; } | ||||
|  | ||||
|   cover::CoverTraits get_traits() override; | ||||
|  | ||||
|  protected: | ||||
|   void control(const cover::CoverCall &call) override; | ||||
|   void stop_prev_trigger_(); | ||||
|   bool is_at_target_() const; | ||||
|   void start_direction_(cover::CoverOperation dir); | ||||
|   void update_operation_(cover::CoverOperation dir); | ||||
|   void endstop_reached_(bool open_endstop); | ||||
|   void recompute_position_(); | ||||
|   void set_current_operation_(cover::CoverOperation operation, bool is_triggered); | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
|   binary_sensor::BinarySensor *open_endstop_{nullptr}; | ||||
|   binary_sensor::BinarySensor *close_endstop_{nullptr}; | ||||
|   binary_sensor::BinarySensor *open_feedback_{nullptr}; | ||||
|   binary_sensor::BinarySensor *close_feedback_{nullptr}; | ||||
|   binary_sensor::BinarySensor *open_obstacle_{nullptr}; | ||||
|   binary_sensor::BinarySensor *close_obstacle_{nullptr}; | ||||
|  | ||||
| #endif | ||||
|   Trigger<> *open_trigger_{new Trigger<>()}; | ||||
|   Trigger<> *close_trigger_{new Trigger<>()}; | ||||
|   Trigger<> *stop_trigger_{new Trigger<>()}; | ||||
|  | ||||
|   uint32_t open_duration_{0}; | ||||
|   uint32_t close_duration_{0}; | ||||
|   uint32_t max_duration_{UINT32_MAX}; | ||||
|   optional<uint32_t> direction_change_waittime_{}; | ||||
|   uint32_t acceleration_wait_time_{0}; | ||||
|   bool has_built_in_endstop_{false}; | ||||
|   bool assumed_state_{false}; | ||||
|   bool infer_endstop_{false}; | ||||
|   float obstacle_rollback_{0}; | ||||
|  | ||||
|   cover::CoverOperation last_operation_{cover::COVER_OPERATION_OPENING}; | ||||
|   cover::CoverOperation current_trigger_operation_{cover::COVER_OPERATION_IDLE}; | ||||
|   Trigger<> *prev_command_trigger_{nullptr}; | ||||
|   uint32_t last_recompute_time_{0}; | ||||
|   uint32_t start_dir_time_{0}; | ||||
|   uint32_t last_publish_time_{0}; | ||||
|   float target_position_{0}; | ||||
|   uint32_t update_interval_{1000}; | ||||
| }; | ||||
|  | ||||
| }  // namespace feedback | ||||
| }  // namespace esphome | ||||
| @@ -44,7 +44,14 @@ template<typename T> class RestoringGlobalsComponent : public Component { | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   void loop() override { | ||||
|   void loop() override { store_value_(); } | ||||
|  | ||||
|   void on_shutdown() override { store_value_(); } | ||||
|  | ||||
|   void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } | ||||
|  | ||||
|  protected: | ||||
|   void store_value_() { | ||||
|     int diff = memcmp(&this->value_, &this->prev_value_, sizeof(T)); | ||||
|     if (diff != 0) { | ||||
|       this->rtc_.save(&this->value_); | ||||
| @@ -52,9 +59,6 @@ template<typename T> class RestoringGlobalsComponent : public Component { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   void set_name_hash(uint32_t name_hash) { this->name_hash_ = name_hash; } | ||||
|  | ||||
|  protected: | ||||
|   T value_{}; | ||||
|   T prev_value_{}; | ||||
|   uint32_t name_hash_{}; | ||||
|   | ||||
| @@ -118,7 +118,7 @@ def _relocate_fields_to_subfolder(config, subfolder, subschema): | ||||
|     fields = [k.schema for k in subschema.schema.keys()] | ||||
|     fields.remove(CONF_ID) | ||||
|     if subfolder in config: | ||||
|         # Ensure no ambigious fields in base of config | ||||
|         # Ensure no ambiguous fields in base of config | ||||
|         for f in fields: | ||||
|             if f in config: | ||||
|                 raise cv.Invalid( | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| #include "hbridge_fan.h" | ||||
| #include "esphome/components/fan/fan_helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| namespace esphome { | ||||
|   | ||||
| @@ -195,7 +195,7 @@ void HydreonRGxxComponent::process_line_() { | ||||
|       if (n == std::string::npos) { | ||||
|         continue; | ||||
|       } | ||||
|       int data = strtol(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr, 10); | ||||
|       float data = strtof(this->buffer_.substr(n + strlen(PROTOCOL_NAMES[i])).c_str(), nullptr); | ||||
|       this->sensors_[i]->publish_state(data); | ||||
|       ESP_LOGD(TAG, "Received %s: %f", PROTOCOL_NAMES[i], this->sensors_[i]->get_raw_state()); | ||||
|       this->sensors_received_ |= (1 << i); | ||||
|   | ||||
| @@ -37,7 +37,7 @@ SUPPORTED_SENSORS = { | ||||
| PROTOCOL_NAMES = { | ||||
|     CONF_MOISTURE: "R", | ||||
|     CONF_ACC: "Acc", | ||||
|     CONF_R_INT: "Rint", | ||||
|     CONF_R_INT: "RInt", | ||||
|     CONF_EVENT_ACC: "EventAcc", | ||||
|     CONF_TOTAL_ACC: "TotalAcc", | ||||
| } | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user